diff --git a/.changeset/validate-rate-limit-period.md b/.changeset/validate-rate-limit-period.md new file mode 100644 index 000000000..d8e7d524b --- /dev/null +++ b/.changeset/validate-rate-limit-period.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +Reject settings where any rateLimits[].period is not greater than 0 diff --git a/src/utils/settings-config.ts b/src/utils/settings-config.ts index f3937b6a7..93fb91498 100644 --- a/src/utils/settings-config.ts +++ b/src/utils/settings-config.ts @@ -570,6 +570,25 @@ export const validatePathAgainstDefaults = (path: string): ValidationIssue[] => ] } +const validateRateLimitPeriods = ( + rateLimits: { period: number }[] | undefined, + pathPrefix: string, + issues: ValidationIssue[], +): void => { + if (!Array.isArray(rateLimits)) { + return + } + rateLimits.forEach((entry, index) => { + if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) { + issues.push({ path: `${pathPrefix}[${index}]`, message: 'rateLimits entry must be an object' }) + return + } + if (!(entry.period > 0)) { + issues.push({ path: `${pathPrefix}[${index}].period`, message: 'period must be greater than 0' }) + } + }) +} + export const validateSettings = (settings: Settings): ValidationIssue[] => { const issues: ValidationIssue[] = [] @@ -644,6 +663,18 @@ export const validateSettings = (settings: Settings): ValidationIssue[] => { } } + const limits = settings.limits + if (limits) { + validateRateLimitPeriods(limits.event?.rateLimits, 'limits.event.rateLimits', issues) + validateRateLimitPeriods(limits.message?.rateLimits, 'limits.message.rateLimits', issues) + validateRateLimitPeriods(limits.connection?.rateLimits, 'limits.connection.rateLimits', issues) + validateRateLimitPeriods(limits.invoice?.rateLimits, 'limits.invoice.rateLimits', issues) + validateRateLimitPeriods(limits.admissionCheck?.rateLimits, 'limits.admissionCheck.rateLimits', issues) + validateRateLimitPeriods(limits.invite?.rateLimits, 'limits.invite.rateLimits', issues) + validateRateLimitPeriods(limits.admin?.rateLimits, 'limits.admin.rateLimits', issues) + validateRateLimitPeriods(limits.admin?.loginRateLimits, 'limits.admin.loginRateLimits', issues) + } + validateShape(loadDefaults(), settings, [], issues) issues.push(...validateAdminNotifications(settings)) diff --git a/test/unit/utils/settings-config.spec.ts b/test/unit/utils/settings-config.spec.ts index bf50ff94f..d9179f35d 100644 --- a/test/unit/utils/settings-config.spec.ts +++ b/test/unit/utils/settings-config.spec.ts @@ -98,6 +98,75 @@ describe('settings-config', () => { expect(issues.some((issue) => issue.path === 'network')).to.equal(true) }) + describe('rate limit period validation', () => { + const baseSettings = () => + ({ + info: { relay_url: 'wss://test.relay', name: 'test' }, + network: {}, + }) as any + + const nonPositivePeriodCases: { issuePath: string; limits: Record }[] = [ + { + issuePath: 'limits.event.rateLimits[0].period', + limits: { event: { rateLimits: [{ period: 0, rate: 1 }] } }, + }, + { + issuePath: 'limits.message.rateLimits[0].period', + limits: { message: { rateLimits: [{ period: 0, rate: 1 }] } }, + }, + { + issuePath: 'limits.connection.rateLimits[0].period', + limits: { connection: { rateLimits: [{ period: -1, rate: 1 }] } }, + }, + { + issuePath: 'limits.invoice.rateLimits[0].period', + limits: { invoice: { rateLimits: [{ period: 0, rate: 1 }] } }, + }, + { + issuePath: 'limits.admissionCheck.rateLimits[0].period', + limits: { admissionCheck: { rateLimits: [{ period: 0, rate: 1 }] } }, + }, + { + issuePath: 'limits.invite.rateLimits[0].period', + limits: { invite: { rateLimits: [{ period: 0, rate: 1 }] } }, + }, + { + issuePath: 'limits.admin.rateLimits[0].period', + limits: { admin: { rateLimits: [{ period: 0, rate: 1 }] } }, + }, + { + issuePath: 'limits.admin.loginRateLimits[0].period', + limits: { admin: { loginRateLimits: [{ period: -1, rate: 1 }] } }, + }, + ] + + for (const { issuePath, limits } of nonPositivePeriodCases) { + it(`rejects a non-positive period on ${issuePath.replace('[0].period', '')}`, () => { + const settings = baseSettings() + settings.limits = limits + + const issues = validateSettings(settings) + expect(issues.some((issue) => issue.path === issuePath)).to.equal(true) + }) + } + + it('accepts a positive period on limits.event.rateLimits', () => { + const settings = baseSettings() + settings.limits = { event: { rateLimits: [{ period: 60_000, rate: 10 }] } } + + const issues = validateSettings(settings) + expect(issues.some((issue) => issue.path.startsWith('limits.event.rateLimits'))).to.equal(false) + }) + + it('rejects a null entry in limits.event.rateLimits without throwing', () => { + const settings = baseSettings() + settings.limits = { event: { rateLimits: [null] } } + + const issues = validateSettings(settings) + expect(issues.some((issue) => issue.path === 'limits.event.rateLimits[0]')).to.equal(true) + }) + }) + describe('adaptive pow settings', () => { const baseSettings = () => ({