diff --git a/docs/CONFIGURATION_DIAGNOSTIC_SNAPSHOT.md b/docs/CONFIGURATION_DIAGNOSTIC_SNAPSHOT.md new file mode 100644 index 00000000..86fd9893 --- /dev/null +++ b/docs/CONFIGURATION_DIAGNOSTIC_SNAPSHOT.md @@ -0,0 +1,51 @@ +# 🔍 Configuration Diagnostic Snapshot Specification + +This document details the sanitized configuration diagnostic representation for NotifyChain (Issue #695). + +--- + +## 1. Overview & Security Guarantee + +When diagnosing operational failures or inspecting runtime health, operators require visibility into runtime parameters, network endpoints, contract bindings, and enabled feature flags without exposing API keys, private keys, or webhook tokens. + +### Security Guarantees: +* **Zero Secret Leakage**: Passwords, private keys (`S...`), and webhook authorization tokens are never included. +* **Redacted Sanitization**: URLs with embedded tokens are masked automatically. + +--- + +## 2. Snapshot Schema + +```json +{ + "system": { + "nodeEnv": "production", + "nodeVersion": "v20.x", + "uptimeSeconds": 3600, + "timestamp": "2026-08-29T12:00:00.000Z" + }, + "network": { + "networkPassphrase": "Test SDF Network ; September 2015", + "rpcUrl": "https://soroban-testnet.stellar.org", + "pollIntervalMs": 5000 + }, + "contracts": { + "configuredCount": 2, + "addresses": ["CA7...", "CB8..."] + }, + "features": { + "analyticsEnabled": true, + "retrySchedulerEnabled": true, + "cleanupEnabled": true, + "deadLetterQueueEnabled": true + }, + "providers": { + "discordEnabled": true, + "webhookEnabled": true + }, + "security": { + "credentialsRedacted": true, + "secretsPresent": true + } +} +``` diff --git a/listener/src/utils/config-snapshot.test.ts b/listener/src/utils/config-snapshot.test.ts new file mode 100644 index 00000000..35e0e6a2 --- /dev/null +++ b/listener/src/utils/config-snapshot.test.ts @@ -0,0 +1,32 @@ +import { createDiagnosticSnapshot } from './config-snapshot'; + +describe('Diagnostic Configuration Snapshot (Issue #695)', () => { + test('generates valid diagnostic snapshot without leaking secrets', () => { + const mockEnv = { + NODE_ENV: 'production', + STELLAR_RPC_URL: 'https://soroban-testnet.stellar.org', + STELLAR_SECRET_KEY: 'SCZANGBA5YHTNYVVV4C3U252E2B6P6IRKD45DCAHSKV2U2B6P6IRKD45', + DISCORD_WEBHOOK_URL: 'https://discord.com/api/webhooks/123/SecretWebhookToken', + CONTRACT_ADDRESSES: JSON.stringify([ + { address: 'CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA64P7TV5A4W' }, + ]), + POLL_INTERVAL_MS: '3000', + }; + + const snapshot = createDiagnosticSnapshot(mockEnv); + + expect(snapshot.system.nodeEnv).toBe('production'); + expect(snapshot.network.pollIntervalMs).toBe(3000); + expect(snapshot.contracts.configuredCount).toBe(1); + expect(snapshot.contracts.addresses).toContain( + 'CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA64P7TV5A4W' + ); + expect(snapshot.providers.discordEnabled).toBe(true); + expect(snapshot.security.credentialsRedacted).toBe(true); + + // Strict Security Invariant: Ensure raw secret string is NOT in the JSON serialization + const serialized = JSON.stringify(snapshot); + expect(serialized).not.toContain('SecretWebhookToken'); + expect(serialized).not.toContain('SCZANGBA5YHTNYVVV4C3U252E2B6P6IRKD45DCAHSKV2U2B6P6IRKD45'); + }); +}); diff --git a/listener/src/utils/config-snapshot.ts b/listener/src/utils/config-snapshot.ts new file mode 100644 index 00000000..5c70b4d2 --- /dev/null +++ b/listener/src/utils/config-snapshot.ts @@ -0,0 +1,109 @@ +/** + * Diagnostic Configuration Snapshot Utility (Issue #695) + * + * Produces a sanitized, security-hardened snapshot of runtime settings + * and environment configurations for operator diagnostics and troubleshooting. + */ + +import { redactSensitiveData, redactString } from './redact'; + +export interface DiagnosticConfigSnapshot { + system: { + nodeEnv: string; + nodeVersion: string; + uptimeSeconds: number; + timestamp: string; + }; + network: { + networkPassphrase: string; + rpcUrl: string; + pollIntervalMs: number; + horizonUrl?: string; + }; + contracts: { + configuredCount: number; + addresses: string[]; + }; + features: { + analyticsEnabled: boolean; + retrySchedulerEnabled: boolean; + cleanupEnabled: boolean; + deadLetterQueueEnabled: boolean; + }; + providers: { + discordEnabled: boolean; + webhookEnabled: boolean; + }; + security: { + credentialsRedacted: boolean; + secretsPresent: boolean; + }; +} + +/** + * Creates a sanitized diagnostic configuration representation. + * All sensitive values, credentials, and API keys are strictly redacted. + */ +export function createDiagnosticSnapshot( + customEnv: Record = process.env +): DiagnosticConfigSnapshot { + const nodeEnv = customEnv.NODE_ENV || 'development'; + const rpcUrl = customEnv.STELLAR_RPC_URL || customEnv.SOROBAN_RPC_URL || 'https://soroban-testnet.stellar.org'; + const passphrase = customEnv.STELLAR_NETWORK_PASSPHRASE || 'Test SDF Network ; September 2015'; + const pollIntervalMs = parseInt(customEnv.POLL_INTERVAL_MS || '5000', 10); + + // Parse configured contract addresses safely + let contractAddresses: string[] = []; + try { + const rawContracts = customEnv.CONTRACT_ADDRESSES; + if (rawContracts) { + const parsed = JSON.parse(rawContracts); + if (Array.isArray(parsed)) { + contractAddresses = parsed.map((c) => (typeof c === 'object' && c?.address ? c.address : String(c))); + } + } + } catch { + // If parse fails, fallback safely + } + + const hasDiscord = Boolean(customEnv.DISCORD_WEBHOOK_URL); + const hasSecrets = Boolean( + customEnv.STELLAR_SECRET_KEY || + customEnv.API_KEYS || + customEnv.JWT_SECRET || + customEnv.DISCORD_WEBHOOK_URL + ); + + return { + system: { + nodeEnv, + nodeVersion: process.version, + uptimeSeconds: Math.floor(process.uptime ? process.uptime() : 0), + timestamp: new Date().toISOString(), + }, + network: { + networkPassphrase: redactString(passphrase), + rpcUrl: redactString(rpcUrl), + pollIntervalMs: isNaN(pollIntervalMs) ? 5000 : pollIntervalMs, + horizonUrl: customEnv.HORIZON_URL ? redactString(customEnv.HORIZON_URL) : undefined, + }, + contracts: { + configuredCount: contractAddresses.length, + addresses: contractAddresses, + }, + features: { + analyticsEnabled: customEnv.ENABLE_ANALYTICS !== 'false', + retrySchedulerEnabled: customEnv.ENABLE_RETRY_SCHEDULER !== 'false', + cleanupEnabled: customEnv.ENABLE_CLEANUP !== 'false', + deadLetterQueueEnabled: customEnv.ENABLE_DLQ !== 'false', + }, + providers: { + discordEnabled: hasDiscord, + webhookEnabled: Boolean(customEnv.WEBHOOK_SECRET || customEnv.WEBHOOK_URL), + }, + security: { + credentialsRedacted: true, + secretsPresent: hasSecrets, + }, + }; +}