From cf5db63d89f07f9e0f41449aa3fad7e163f5d259 Mon Sep 17 00:00:00 2001 From: Yungmon22 <101454304+Yungmon22@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:24:26 +0100 Subject: [PATCH 1/3] security: Require string values for required security secrets (#636) --- tests/utils/server-config-schema.test.ts | 46 +++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/utils/server-config-schema.test.ts b/tests/utils/server-config-schema.test.ts index e5202ccd..6476379a 100644 --- a/tests/utils/server-config-schema.test.ts +++ b/tests/utils/server-config-schema.test.ts @@ -1,5 +1,5 @@ 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', () => { @@ -109,6 +109,50 @@ describe('ServerConfigSchema', () => { }); }); +describe('SecurityConfigSchema', () => { + describe('sep10SigningKey field', () => { + it('validates non-empty string secrets', () => { + expect(SecurityConfigSchema.sep10SigningKey.validate('SABCDEF123')).toBe(true); + }); + + it('rejects missing, empty, whitespace-only, and non-string secrets', () => { + expect(SecurityConfigSchema.sep10SigningKey.validate(undefined)).toBe(false); + expect(SecurityConfigSchema.sep10SigningKey.validate('')).toBe(false); + expect(SecurityConfigSchema.sep10SigningKey.validate(' ')).toBe(false); + expect(SecurityConfigSchema.sep10SigningKey.validate(123)).toBe(false); + expect(SecurityConfigSchema.sep10SigningKey.validate({})).toBe(false); + }); + }); + + describe('interactiveJwtSecret field', () => { + it('validates non-empty string secrets', () => { + expect(SecurityConfigSchema.interactiveJwtSecret.validate('jwt-secret')).toBe(true); + }); + + it('rejects missing, empty, whitespace-only, and non-string secrets', () => { + expect(SecurityConfigSchema.interactiveJwtSecret.validate(undefined)).toBe(false); + expect(SecurityConfigSchema.interactiveJwtSecret.validate('')).toBe(false); + expect(SecurityConfigSchema.interactiveJwtSecret.validate(' ')).toBe(false); + expect(SecurityConfigSchema.interactiveJwtSecret.validate(123)).toBe(false); + expect(SecurityConfigSchema.interactiveJwtSecret.validate({})).toBe(false); + }); + }); + + describe('distributionAccountSecret field', () => { + it('validates non-empty string secrets', () => { + expect(SecurityConfigSchema.distributionAccountSecret.validate('SABCDEF123')).toBe(true); + }); + + it('rejects missing, empty, whitespace-only, and non-string secrets', () => { + expect(SecurityConfigSchema.distributionAccountSecret.validate(undefined)).toBe(false); + expect(SecurityConfigSchema.distributionAccountSecret.validate('')).toBe(false); + expect(SecurityConfigSchema.distributionAccountSecret.validate(' ')).toBe(false); + expect(SecurityConfigSchema.distributionAccountSecret.validate(123)).toBe(false); + expect(SecurityConfigSchema.distributionAccountSecret.validate({})).toBe(false); + }); + }); +}); + describe('validateServerConfig', () => { it('returns empty array for valid config', () => { const errors = validateServerConfig({ From 1ef55a79018b2c2ae6e594cda16345c028612e09 Mon Sep 17 00:00:00 2001 From: Yungmon22 <101454304+Yungmon22@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:26:25 +0100 Subject: [PATCH 2/3] security: Require string values for required security secrets (#636) --- src/utils/validation.ts | 6 ++ tests/utils/server-config-schema.test.ts | 86 +++++++++++------------- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/utils/validation.ts b/src/utils/validation.ts index bf1fa52d..c92d5869 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -1 +1,7 @@ export * from './validation-helpers.ts'; + +export const SecurityConfigSchema = z.object({ + sep10SigningKey: z.string().trim().min(1), + interactiveJwtSecret: z.string().trim().min(1), + distributionAccountSecret: z.string().trim().min(1), +}); diff --git a/tests/utils/server-config-schema.test.ts b/tests/utils/server-config-schema.test.ts index 6476379a..18900a87 100644 --- a/tests/utils/server-config-schema.test.ts +++ b/tests/utils/server-config-schema.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { SecurityConfigSchema, ServerConfigSchema, validateServerConfig } from '../../src/utils/validation'; +import { ServerConfigSchema, validateServerConfig } from '../../src/utils/validation'; describe('ServerConfigSchema', () => { it('is publicly importable from validation module', () => { @@ -109,50 +109,6 @@ describe('ServerConfigSchema', () => { }); }); -describe('SecurityConfigSchema', () => { - describe('sep10SigningKey field', () => { - it('validates non-empty string secrets', () => { - expect(SecurityConfigSchema.sep10SigningKey.validate('SABCDEF123')).toBe(true); - }); - - it('rejects missing, empty, whitespace-only, and non-string secrets', () => { - expect(SecurityConfigSchema.sep10SigningKey.validate(undefined)).toBe(false); - expect(SecurityConfigSchema.sep10SigningKey.validate('')).toBe(false); - expect(SecurityConfigSchema.sep10SigningKey.validate(' ')).toBe(false); - expect(SecurityConfigSchema.sep10SigningKey.validate(123)).toBe(false); - expect(SecurityConfigSchema.sep10SigningKey.validate({})).toBe(false); - }); - }); - - describe('interactiveJwtSecret field', () => { - it('validates non-empty string secrets', () => { - expect(SecurityConfigSchema.interactiveJwtSecret.validate('jwt-secret')).toBe(true); - }); - - it('rejects missing, empty, whitespace-only, and non-string secrets', () => { - expect(SecurityConfigSchema.interactiveJwtSecret.validate(undefined)).toBe(false); - expect(SecurityConfigSchema.interactiveJwtSecret.validate('')).toBe(false); - expect(SecurityConfigSchema.interactiveJwtSecret.validate(' ')).toBe(false); - expect(SecurityConfigSchema.interactiveJwtSecret.validate(123)).toBe(false); - expect(SecurityConfigSchema.interactiveJwtSecret.validate({})).toBe(false); - }); - }); - - describe('distributionAccountSecret field', () => { - it('validates non-empty string secrets', () => { - expect(SecurityConfigSchema.distributionAccountSecret.validate('SABCDEF123')).toBe(true); - }); - - it('rejects missing, empty, whitespace-only, and non-string secrets', () => { - expect(SecurityConfigSchema.distributionAccountSecret.validate(undefined)).toBe(false); - expect(SecurityConfigSchema.distributionAccountSecret.validate('')).toBe(false); - expect(SecurityConfigSchema.distributionAccountSecret.validate(' ')).toBe(false); - expect(SecurityConfigSchema.distributionAccountSecret.validate(123)).toBe(false); - expect(SecurityConfigSchema.distributionAccountSecret.validate({})).toBe(false); - }); - }); -}); - describe('validateServerConfig', () => { it('returns empty array for valid config', () => { const errors = validateServerConfig({ @@ -201,3 +157,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); + }); + }); + } +}); From d8b1e76751e7838e320ee3d9d721478acb5d60a0 Mon Sep 17 00:00:00 2001 From: Ikemhood <91676649+ikemHood@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:25:22 +0100 Subject: [PATCH 3/3] fix: validate required security secrets as strings --- src/utils/validation-helpers.ts | 35 +++++++++++++++++++++--- src/utils/validation.ts | 6 ---- tests/utils/server-config-schema.test.ts | 6 +++- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/utils/validation-helpers.ts b/src/utils/validation-helpers.ts index 5cf715b2..f555a8d4 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/src/utils/validation.ts b/src/utils/validation.ts index c92d5869..bf1fa52d 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -1,7 +1 @@ export * from './validation-helpers.ts'; - -export const SecurityConfigSchema = z.object({ - sep10SigningKey: z.string().trim().min(1), - interactiveJwtSecret: z.string().trim().min(1), - distributionAccountSecret: z.string().trim().min(1), -}); diff --git a/tests/utils/server-config-schema.test.ts b/tests/utils/server-config-schema.test.ts index 18900a87..c7cf5cdd 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', () => {