diff --git a/src/utils/validation-helpers.ts b/src/utils/validation-helpers.ts index 5cf715b..f555a8d 100644 --- a/src/utils/validation-helpers.ts +++ b/src/utils/validation-helpers.ts @@ -499,14 +499,41 @@ export const NetworkConfigSchema = { /** * SecurityConfigSchema - Public validation helper for security configuration. */ -export const SecurityConfigSchema = { +type RequiredSecuritySecret = + | 'sep10SigningKey' + | 'interactiveJwtSecret' + | 'distributionAccountSecret'; + +type SecurityConfigSchemaDefinition = Record & { + validate(config: SecurityConfig): void; +}; + +export const SecurityConfigSchema: SecurityConfigSchemaDefinition = { + sep10SigningKey: { + type: 'string', + required: true, + description: 'SEP-10 server signing secret.', + validate: (value) => typeof value === 'string' && value.trim().length > 0, + }, + interactiveJwtSecret: { + type: 'string', + required: true, + description: 'Secret used to sign interactive JWTs.', + validate: (value) => typeof value === 'string' && value.trim().length > 0, + }, + distributionAccountSecret: { + type: 'string', + required: true, + description: 'Distribution account secret.', + validate: (value) => typeof value === 'string' && value.trim().length > 0, + }, validate(config: SecurityConfig): void { if (!config) throw new Error('Missing required field: security'); - if (!config.sep10SigningKey) + if (!SecurityConfigSchema.sep10SigningKey.validate(config.sep10SigningKey)) throw new Error('Missing required secret: security.sep10SigningKey'); - if (!config.interactiveJwtSecret) + if (!SecurityConfigSchema.interactiveJwtSecret.validate(config.interactiveJwtSecret)) throw new Error('Missing required secret: security.interactiveJwtSecret'); - if (!config.distributionAccountSecret) + if (!SecurityConfigSchema.distributionAccountSecret.validate(config.distributionAccountSecret)) throw new Error('Missing required secret: security.distributionAccountSecret'); if ( config.enableClientAttribution !== undefined && diff --git a/tests/utils/server-config-schema.test.ts b/tests/utils/server-config-schema.test.ts index e5202cc..c7cf5cd 100644 --- a/tests/utils/server-config-schema.test.ts +++ b/tests/utils/server-config-schema.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { ServerConfigSchema, validateServerConfig } from '../../src/utils/validation'; +import { + SecurityConfigSchema, + ServerConfigSchema, + validateServerConfig, +} from '../../src/utils/validation'; describe('ServerConfigSchema', () => { it('is publicly importable from validation module', () => { @@ -157,3 +161,43 @@ describe('ServerConfigSchema public export', () => { expect(mod.ServerConfigSchema).toBeDefined(); }); }); + +describe('SecurityConfigSchema', () => { + const requiredSecretFields = [ + 'sep10SigningKey', + 'interactiveJwtSecret', + 'distributionAccountSecret', + ] as const; + + it('exposes the required secret fields', () => { + for (const field of requiredSecretFields) { + expect(SecurityConfigSchema).toHaveProperty(field); + expect(typeof SecurityConfigSchema[field].validate).toBe('function'); + } + }); + + for (const field of requiredSecretFields) { + describe(`${field} field`, () => { + it('accepts a non-empty string without modifying it', () => { + const value = 's'.repeat(56); + expect(SecurityConfigSchema[field].validate(value)).toBe(true); + expect(value).toBe('s'.repeat(56)); + }); + + it('rejects missing, empty, and whitespace-only values', () => { + expect(SecurityConfigSchema[field].validate(undefined)).toBe(false); + expect(SecurityConfigSchema[field].validate(null)).toBe(false); + expect(SecurityConfigSchema[field].validate('')).toBe(false); + expect(SecurityConfigSchema[field].validate(' ')).toBe(false); + expect(SecurityConfigSchema[field].validate('\t\n')).toBe(false); + }); + + it('rejects non-string values', () => { + expect(SecurityConfigSchema[field].validate(123)).toBe(false); + expect(SecurityConfigSchema[field].validate(true)).toBe(false); + expect(SecurityConfigSchema[field].validate({})).toBe(false); + expect(SecurityConfigSchema[field].validate([])).toBe(false); + }); + }); + } +});