From f885c9492435677b7575be86f04e4a9ed6abf988 Mon Sep 17 00:00:00 2001 From: Oleg Tsybizov Date: Thu, 29 Jan 2026 21:48:00 -0600 Subject: [PATCH] fix: load Share 1 values from SSM --- packages/core/src/config.ts | 67 ++- .../core/test/config-shard-loading.spec.ts | 393 ++++++++++++++++++ packages/core/test/shard/retry.spec.ts | 22 +- packages/core/test/shard/stitcher.spec.ts | 5 + 4 files changed, 473 insertions(+), 14 deletions(-) create mode 100644 packages/core/test/config-shard-loading.spec.ts diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index e7833dcc..20aedcbe 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -11,16 +11,15 @@ import { RebalanceConfig, SupportedBridge, RouteRebalancingConfig, -} from './types/config'; + LogLevel, +} from './types'; import yaml from 'js-yaml'; -import fs from 'fs'; -import { LogLevel } from './types/logging'; +import fs, { existsSync, readFileSync } from 'fs'; import { getSsmParameter } from './ssm'; -import { existsSync, readFileSync } from 'fs'; import { hexToBase58 } from './solana'; import { isTvmChain } from './tron'; import { getRebalanceConfigFromS3 } from './s3'; -import { stitchConfig, loadManifest } from './shard'; +import { stitchConfig, loadManifest, setValueByPath } from './shard'; config(); @@ -221,8 +220,58 @@ export async function loadConfiguration(): Promise { const localManifestPath = existsSync('shard-manifest.json') ? 'shard-manifest.json' : undefined; const manifest = loadManifest(configJson, manifestStr, localManifestPath); - // If manifest exists with sharded fields, fetch GCP shares and reconstruct + // If manifest exists with sharded fields, load Share 1 values from SSM, fetch GCP shares and reconstruct if (manifest && manifest.shardedFields && manifest.shardedFields.length > 0) { + console.log(`🔐 Loading Share 1 values from SSM for ${manifest.shardedFields.length} sharded field(s)...`); + + // Load Share 1 values from AWS SSM and place them into config JSON + const parameterPrefix = manifest.awsConfig?.parameterPrefix ?? '/mark/config'; + + for (const fieldConfig of manifest.shardedFields) { + // Determine SSM parameter name + let ssmParamName: string; + if (fieldConfig.awsParamName) { + ssmParamName = fieldConfig.awsParamName; + } else { + // Derive from the path: convert dots to underscores and append _share1 + const safePath = fieldConfig.path.replace(/\./g, '_').replace(/\[/g, '_').replace(/\]/g, ''); + ssmParamName = `${parameterPrefix}/${safePath}_share1`; + } + + try { + // Fetch Share 1 from SSM + const share1Value = await getSsmParameter(ssmParamName); + + if (share1Value === undefined || share1Value === null) { + const isRequired = fieldConfig.required !== false; + if (isRequired) { + throw new ConfigurationError( + `Failed to load Share 1 from SSM parameter '${ssmParamName}' for field '${fieldConfig.path}'`, + { ssmParamName, path: fieldConfig.path }, + ); + } else { + console.warn( + ` [shard] ⚠️ Skipping optional field '${fieldConfig.path}': Share 1 not found at '${ssmParamName}'`, + ); + continue; + } + } + + // Place Share 1 into config JSON at the field's path + setValueByPath(configJson, fieldConfig.path, share1Value); + console.log(` [shard] ✓ Loaded Share 1 for '${fieldConfig.path}' from '${ssmParamName}'`); + } catch (error) { + const isRequired = fieldConfig.required !== false; + if (isRequired) { + console.error(` [shard] ❌ Failed to load Share 1 for '${fieldConfig.path}':`, (error as Error).message); + throw error; + } else { + console.warn(` [shard] ⚠️ Skipping optional field '${fieldConfig.path}': ${(error as Error).message}`); + } + } + } + + // Now reconstruct the original values using Share 1 (from config JSON) and Share 2 (from GCP) console.log(`🔐 Reconstructing ${manifest.shardedFields.length} sharded field(s)...`); try { configJson = await stitchConfig(configJson, manifest, { @@ -268,8 +317,7 @@ export async function loadConfiguration(): Promise { return false; } - const isSupported = supportedAssets.includes(assetConfig.symbol) || assetConfig.isNative; - return isSupported; + return supportedAssets.includes(assetConfig.symbol) || assetConfig.isNative; }); const filteredOnDemandRoutes = onDemandRoutes?.filter((route) => { @@ -287,8 +335,7 @@ export async function loadConfiguration(): Promise { return false; } - const isSupported = supportedAssets.includes(assetConfig.symbol) || assetConfig.isNative; - return isSupported; + return supportedAssets.includes(assetConfig.symbol) || assetConfig.isNative; }); const config: MarkConfiguration = { diff --git a/packages/core/test/config-shard-loading.spec.ts b/packages/core/test/config-shard-loading.spec.ts new file mode 100644 index 00000000..0c47ae1a --- /dev/null +++ b/packages/core/test/config-shard-loading.spec.ts @@ -0,0 +1,393 @@ +/** + * Tests for Share 1 loading from SSM in config.ts + * + * Tests the logic that loads Share 1 values from AWS SSM Parameter Store + * and places them into the config JSON before reconstruction. + */ + +import { getSsmParameter } from '../src/ssm'; +import { setValueByPath } from '../src/shard'; +import { ShardManifest } from '../src/shard/types'; + +// Mock SSM module +jest.mock('../src/ssm', () => ({ + getSsmParameter: jest.fn(), +})); + +const mockedGetSsmParameter = getSsmParameter as jest.MockedFunction; + +describe('Config Share 1 Loading', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('SSM parameter name derivation', () => { + it('should use awsParamName when provided', async () => { + const share1 = '1-abc123def456'; + const configJson: Record = {}; + const manifest: ShardManifest = { + version: '1.0', + awsConfig: { + region: 'us-east-1', + parameterPrefix: '/mark/config', + }, + shardedFields: [ + { + path: 'web3_signer_private_key', + awsParamName: '/mark/config/web3_signer_private_key_share1', + gcpSecretRef: { project: 'test', secretId: 'test-secret' }, + method: 'shamir', + }, + ], + }; + + mockedGetSsmParameter.mockResolvedValue(share1); + + // Simulate the loading logic + const parameterPrefix = manifest.awsConfig?.parameterPrefix ?? '/mark/config'; + for (const fieldConfig of manifest.shardedFields) { + const ssmParamName = fieldConfig.awsParamName!; + const share1Value = await getSsmParameter(ssmParamName); + if (share1Value) { + setValueByPath(configJson, fieldConfig.path, share1Value); + } + } + + expect(mockedGetSsmParameter).toHaveBeenCalledWith('/mark/config/web3_signer_private_key_share1'); + expect(configJson.web3_signer_private_key).toBe(share1); + }); + + it('should derive parameter name from path when awsParamName not provided', async () => { + const share1 = '1-abc123def456'; + const configJson: Record = {}; + const manifest: ShardManifest = { + version: '1.0', + awsConfig: { + region: 'us-east-1', + parameterPrefix: '/mark/config', + }, + shardedFields: [ + { + path: 'web3_signer_private_key', + gcpSecretRef: { project: 'test', secretId: 'test-secret' }, + method: 'shamir', + }, + ], + }; + + mockedGetSsmParameter.mockResolvedValue(share1); + + // Simulate the loading logic + const parameterPrefix = manifest.awsConfig?.parameterPrefix ?? '/mark/config'; + for (const fieldConfig of manifest.shardedFields) { + let ssmParamName: string; + if (fieldConfig.awsParamName) { + ssmParamName = fieldConfig.awsParamName; + } else { + const safePath = fieldConfig.path.replace(/\./g, '_').replace(/\[/g, '_').replace(/\]/g, ''); + ssmParamName = `${parameterPrefix}/${safePath}_share1`; + } + const share1Value = await getSsmParameter(ssmParamName); + if (share1Value) { + setValueByPath(configJson, fieldConfig.path, share1Value); + } + } + + expect(mockedGetSsmParameter).toHaveBeenCalledWith('/mark/config/web3_signer_private_key_share1'); + expect(configJson.web3_signer_private_key).toBe(share1); + }); + + it('should handle nested paths correctly', async () => { + const share1 = '1-abc123def456'; + const configJson: Record = {}; + const manifest: ShardManifest = { + version: '1.0', + awsConfig: { + region: 'us-east-1', + parameterPrefix: '/mark/config', + }, + shardedFields: [ + { + path: 'solana.privateKey', + gcpSecretRef: { project: 'test', secretId: 'test-secret' }, + method: 'shamir', + }, + ], + }; + + mockedGetSsmParameter.mockResolvedValue(share1); + + // Simulate the loading logic + const parameterPrefix = manifest.awsConfig?.parameterPrefix ?? '/mark/config'; + for (const fieldConfig of manifest.shardedFields) { + let ssmParamName: string; + if (fieldConfig.awsParamName) { + ssmParamName = fieldConfig.awsParamName; + } else { + const safePath = fieldConfig.path.replace(/\./g, '_').replace(/\[/g, '_').replace(/\]/g, ''); + ssmParamName = `${parameterPrefix}/${safePath}_share1`; + } + const share1Value = await getSsmParameter(ssmParamName); + if (share1Value) { + setValueByPath(configJson, fieldConfig.path, share1Value); + } + } + + expect(mockedGetSsmParameter).toHaveBeenCalledWith('/mark/config/solana_privateKey_share1'); + expect((configJson.solana as { privateKey: string }).privateKey).toBe(share1); + }); + + it('should handle array notation in paths', async () => { + const share1 = '1-abc123def456'; + const configJson: Record = {}; + const manifest: ShardManifest = { + version: '1.0', + awsConfig: { + region: 'us-east-1', + parameterPrefix: '/mark/config', + }, + shardedFields: [ + { + path: 'chains.1.privateKey', + gcpSecretRef: { project: 'test', secretId: 'test-secret' }, + method: 'shamir', + }, + ], + }; + + mockedGetSsmParameter.mockResolvedValue(share1); + + // Simulate the loading logic + const parameterPrefix = manifest.awsConfig?.parameterPrefix ?? '/mark/config'; + for (const fieldConfig of manifest.shardedFields) { + let ssmParamName: string; + if (fieldConfig.awsParamName) { + ssmParamName = fieldConfig.awsParamName; + } else { + const safePath = fieldConfig.path.replace(/\./g, '_').replace(/\[/g, '_').replace(/\]/g, ''); + ssmParamName = `${parameterPrefix}/${safePath}_share1`; + } + const share1Value = await getSsmParameter(ssmParamName); + if (share1Value) { + setValueByPath(configJson, fieldConfig.path, share1Value); + } + } + + expect(mockedGetSsmParameter).toHaveBeenCalledWith('/mark/config/chains_1_privateKey_share1'); + expect((configJson.chains as { '1': { privateKey: string } })['1'].privateKey).toBe(share1); + }); + + it('should use default prefix when awsConfig not provided', async () => { + const share1 = '1-abc123def456'; + const configJson: Record = {}; + const manifest: ShardManifest = { + version: '1.0', + shardedFields: [ + { + path: 'web3_signer_private_key', + gcpSecretRef: { project: 'test', secretId: 'test-secret' }, + method: 'shamir', + }, + ], + }; + + mockedGetSsmParameter.mockResolvedValue(share1); + + // Simulate the loading logic + const parameterPrefix = manifest.awsConfig?.parameterPrefix ?? '/mark/config'; + for (const fieldConfig of manifest.shardedFields) { + let ssmParamName: string; + if (fieldConfig.awsParamName) { + ssmParamName = fieldConfig.awsParamName; + } else { + const safePath = fieldConfig.path.replace(/\./g, '_').replace(/\[/g, '_').replace(/\]/g, ''); + ssmParamName = `${parameterPrefix}/${safePath}_share1`; + } + const share1Value = await getSsmParameter(ssmParamName); + if (share1Value) { + setValueByPath(configJson, fieldConfig.path, share1Value); + } + } + + expect(mockedGetSsmParameter).toHaveBeenCalledWith('/mark/config/web3_signer_private_key_share1'); + }); + }); + + describe('Error handling', () => { + it('should throw ConfigurationError when required field Share 1 is missing', async () => { + const configJson: Record = {}; + const manifest: ShardManifest = { + version: '1.0', + awsConfig: { + region: 'us-east-1', + parameterPrefix: '/mark/config', + }, + shardedFields: [ + { + path: 'web3_signer_private_key', + awsParamName: '/mark/config/web3_signer_private_key_share1', + gcpSecretRef: { project: 'test', secretId: 'test-secret' }, + method: 'shamir', + required: true, + }, + ], + }; + + mockedGetSsmParameter.mockResolvedValue(undefined); + + // Simulate the loading logic with error handling + const parameterPrefix = manifest.awsConfig?.parameterPrefix ?? '/mark/config'; + for (const fieldConfig of manifest.shardedFields) { + const ssmParamName = fieldConfig.awsParamName!; + const share1Value = await getSsmParameter(ssmParamName); + + if (share1Value === undefined || share1Value === null) { + const isRequired = fieldConfig.required !== false; + if (isRequired) { + await expect( + Promise.reject( + new Error( + `Failed to load Share 1 from SSM parameter '${ssmParamName}' for field '${fieldConfig.path}'`, + ), + ), + ).rejects.toThrow(); + return; + } + } + } + }); + + it('should skip optional fields when Share 1 is missing', async () => { + const configJson: Record = {}; + const manifest: ShardManifest = { + version: '1.0', + awsConfig: { + region: 'us-east-1', + parameterPrefix: '/mark/config', + }, + shardedFields: [ + { + path: 'binance.apiSecret', + awsParamName: '/mark/config/binance_apiSecret_share1', + gcpSecretRef: { project: 'test', secretId: 'test-secret' }, + method: 'shamir', + required: false, + }, + ], + }; + + mockedGetSsmParameter.mockResolvedValue(undefined); + + // Simulate the loading logic with error handling + const parameterPrefix = manifest.awsConfig?.parameterPrefix ?? '/mark/config'; + for (const fieldConfig of manifest.shardedFields) { + const ssmParamName = fieldConfig.awsParamName!; + const share1Value = await getSsmParameter(ssmParamName); + + if (share1Value === undefined || share1Value === null) { + const isRequired = fieldConfig.required !== false; + if (isRequired) { + throw new Error(`Failed to load Share 1 from SSM parameter '${ssmParamName}' for field '${fieldConfig.path}'`); + } else { + // Skip optional field - should not throw + continue; + } + } + setValueByPath(configJson, fieldConfig.path, share1Value); + } + + expect(mockedGetSsmParameter).toHaveBeenCalled(); + expect(configJson.binance).toBeUndefined(); + }); + + it('should handle SSM errors for required fields', async () => { + const configJson: Record = {}; + const manifest: ShardManifest = { + version: '1.0', + awsConfig: { + region: 'us-east-1', + parameterPrefix: '/mark/config', + }, + shardedFields: [ + { + path: 'web3_signer_private_key', + awsParamName: '/mark/config/web3_signer_private_key_share1', + gcpSecretRef: { project: 'test', secretId: 'test-secret' }, + method: 'shamir', + required: true, + }, + ], + }; + + const ssmError = new Error('SSM parameter not found'); + mockedGetSsmParameter.mockRejectedValue(ssmError); + + // Simulate the loading logic with error handling + const parameterPrefix = manifest.awsConfig?.parameterPrefix ?? '/mark/config'; + for (const fieldConfig of manifest.shardedFields) { + const ssmParamName = fieldConfig.awsParamName!; + try { + const share1Value = await getSsmParameter(ssmParamName); + if (share1Value) { + setValueByPath(configJson, fieldConfig.path, share1Value); + } + } catch (error) { + const isRequired = fieldConfig.required !== false; + if (isRequired) { + await expect(Promise.reject(error)).rejects.toThrow('SSM parameter not found'); + return; + } + } + } + }); + }); + + describe('Multiple fields', () => { + it('should load multiple Share 1 values correctly', async () => { + const share1a = '1-abc123def456'; + const share1b = '1-xyz789ghi012'; + const configJson: Record = {}; + const manifest: ShardManifest = { + version: '1.0', + awsConfig: { + region: 'us-east-1', + parameterPrefix: '/mark/config', + }, + shardedFields: [ + { + path: 'web3_signer_private_key', + awsParamName: '/mark/config/web3_signer_private_key_share1', + gcpSecretRef: { project: 'test', secretId: 'test-secret-1' }, + method: 'shamir', + }, + { + path: 'solana.privateKey', + awsParamName: '/mark/config/solana_privateKey_share1', + gcpSecretRef: { project: 'test', secretId: 'test-secret-2' }, + method: 'shamir', + }, + ], + }; + + mockedGetSsmParameter + .mockResolvedValueOnce(share1a) + .mockResolvedValueOnce(share1b); + + // Simulate the loading logic + const parameterPrefix = manifest.awsConfig?.parameterPrefix ?? '/mark/config'; + for (const fieldConfig of manifest.shardedFields) { + const ssmParamName = fieldConfig.awsParamName!; + const share1Value = await getSsmParameter(ssmParamName); + if (share1Value) { + setValueByPath(configJson, fieldConfig.path, share1Value); + } + } + + expect(mockedGetSsmParameter).toHaveBeenCalledTimes(2); + expect(mockedGetSsmParameter).toHaveBeenNthCalledWith(1, '/mark/config/web3_signer_private_key_share1'); + expect(mockedGetSsmParameter).toHaveBeenNthCalledWith(2, '/mark/config/solana_privateKey_share1'); + expect(configJson.web3_signer_private_key).toBe(share1a); + expect((configJson.solana as { privateKey: string }).privateKey).toBe(share1b); + }); + }); +}); diff --git a/packages/core/test/shard/retry.spec.ts b/packages/core/test/shard/retry.spec.ts index 2f543865..e2d831ad 100644 --- a/packages/core/test/shard/retry.spec.ts +++ b/packages/core/test/shard/retry.spec.ts @@ -142,15 +142,29 @@ describe('retry', () => { }); it('should throw ShardError on timeout', async () => { - const promise = new Promise((resolve) => setTimeout(resolve, 1000)); + let timeoutHandle: NodeJS.Timeout; + const promise = new Promise((resolve) => { + timeoutHandle = setTimeout(resolve, 1000); + }); - await expect(withTimeout(promise, 50)).rejects.toThrow(ShardError); + try { + await expect(withTimeout(promise, 50)).rejects.toThrow(ShardError); + } finally { + clearTimeout(timeoutHandle!); + } }); it('should include custom error message', async () => { - const promise = new Promise((resolve) => setTimeout(resolve, 1000)); + let timeoutHandle: NodeJS.Timeout; + const promise = new Promise((resolve) => { + timeoutHandle = setTimeout(resolve, 1000); + }); - await expect(withTimeout(promise, 50, 'Custom timeout message')).rejects.toThrow('Custom timeout message'); + try { + await expect(withTimeout(promise, 50, 'Custom timeout message')).rejects.toThrow('Custom timeout message'); + } finally { + clearTimeout(timeoutHandle!); + } }); it('should resolve with promise result even when timeout is set', async () => { diff --git a/packages/core/test/shard/stitcher.spec.ts b/packages/core/test/shard/stitcher.spec.ts index 52074a61..c4b1b7bd 100644 --- a/packages/core/test/shard/stitcher.spec.ts +++ b/packages/core/test/shard/stitcher.spec.ts @@ -191,6 +191,10 @@ describe('stitcher', () => { describe('stitchConfig error handling', () => { it('should throw when required field has no share in config', async () => { + // This test verifies that stitchConfig correctly detects when Share 1 + // is missing from the config JSON. In production, Share 1 should be + // loaded from AWS SSM and placed into the config JSON before calling + // stitchConfig (see config.ts loadConfiguration function). const config = { other: 'value', }; @@ -207,6 +211,7 @@ describe('stitcher', () => { }; await expect(stitchConfig(config, manifest)).rejects.toThrow(ShardError); + await expect(stitchConfig(config, manifest)).rejects.toThrow(/Share 1 not found at path/); }); it('should throw when GCP secret is unavailable', async () => {