feat(security): share rate limits across instances, drain syncs on shutdown

- Rate limiting counters live in PostgreSQL (throttler_records), shared by
  every instance, one atomic upsert per request; fails open.
- On shutdown, new syncs are refused (503) and the running one is waited for
  up to SYNC_SHUTDOWN_TIMEOUT; Prisma disconnects last.
- Hiboutik calls never follow redirects and are capped at 50 MB.
- Prisma errors no longer print query arguments (errorFormat minimal).
- In production, DATABASE_URL must require TLS.
- The sync lock connection times out after 10 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MastersGame
2026-11-08 12:21:15 +01:00
parent 35a7c27cb7
commit 3dd5369175
14 changed files with 414 additions and 34 deletions

View File

@@ -36,4 +36,35 @@ describe('validateEnv', () => {
expect((error as Error).message).not.toContain('secret');
}
});
describe('in production', () => {
const production = { ...valid, NODE_ENV: 'production' };
it('requires an encrypted database connection', () => {
expect(() => validateEnv(production)).toThrow(/DATABASE_URL: sslmode/);
expect(() => validateEnv({ ...production, DATABASE_URL: `${valid.DATABASE_URL}?sslmode=disable` })).toThrow(/DATABASE_URL/);
});
it.each(['require', 'verify-ca', 'verify-full'])('accepts sslmode=%s', (mode) => {
expect(() => validateEnv({ ...production, DATABASE_URL: `${valid.DATABASE_URL}?sslmode=${mode}` })).not.toThrow();
});
it('never puts the connection string in the error message', () => {
expect.assertions(2);
try {
validateEnv(production);
} catch (error) {
expect((error as Error).message).toContain('DATABASE_URL');
expect((error as Error).message).not.toContain('secret');
}
});
});
it('does not require TLS outside production', () => {
expect(() => validateEnv({ ...valid, NODE_ENV: 'development' })).not.toThrow();
});
it('rejects an unknown NODE_ENV', () => {
expect(() => validateEnv({ ...valid, NODE_ENV: 'prod' })).toThrow(/NODE_ENV/);
});
});