feat(security): harden the API, trust Hiboutik for deletions, lock syncs (#3)
- Sync deletions: a 2xx Hiboutik answer is the truth, even an empty list; a failed call (HTTP error or non-list body) deletes nothing (HiboutikClient.getList). - Validate the environment at startup (HIBOUTIK_BASE_URL must be HTTPS), global ValidationPipe, Helmet, CORS opt-in, HTTP timeouts, rate limiting (60 req/min/IP, 5 on POST /sync), AllExceptionsFilter. - Postgres advisory lock so only one synchronization runs at a time across instances; syncAll is private, syncs start through startSync. - Fix the Jest moduleNameMapper; document the validation and security rules in CLAUDE.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
39
src/config/env.validation.spec.ts
Normal file
39
src/config/env.validation.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'reflect-metadata';
|
||||
import { validateEnv } from './env.validation';
|
||||
|
||||
describe('validateEnv', () => {
|
||||
const valid = {
|
||||
DATABASE_URL: 'postgresql://user:secret@localhost:5432/db',
|
||||
HIBOUTIK_BASE_URL: 'https://shop.hiboutik.com/api',
|
||||
HIBOUTIK_USERNAME: 'user',
|
||||
HIBOUTIK_PASSWORD: 'secret-password',
|
||||
};
|
||||
|
||||
it('accepts a valid configuration and keeps every variable', () => {
|
||||
const env = { ...valid, PORT: '3000', CORS_ORIGINS: 'https://shop.example.com', TRUST_PROXY: '1', OTHER: 'x' };
|
||||
expect(validateEnv(env)).toBe(env);
|
||||
});
|
||||
|
||||
it('rejects a missing required variable, naming it', () => {
|
||||
expect(() => validateEnv({ ...valid, HIBOUTIK_PASSWORD: undefined })).toThrow(/HIBOUTIK_PASSWORD/);
|
||||
});
|
||||
|
||||
it('rejects a non-HTTPS Hiboutik URL', () => {
|
||||
expect(() => validateEnv({ ...valid, HIBOUTIK_BASE_URL: 'http://shop.hiboutik.com/api' })).toThrow(/HIBOUTIK_BASE_URL/);
|
||||
});
|
||||
|
||||
it('rejects an invalid port', () => {
|
||||
expect(() => validateEnv({ ...valid, PORT: 'abc' })).toThrow(/PORT/);
|
||||
expect(() => validateEnv({ ...valid, PORT: '70000' })).toThrow(/PORT/);
|
||||
});
|
||||
|
||||
it('never puts secret values in the error message', () => {
|
||||
expect.assertions(2);
|
||||
try {
|
||||
validateEnv({ ...valid, HIBOUTIK_BASE_URL: 'http://insecure', DATABASE_URL: '' });
|
||||
} catch (error) {
|
||||
expect((error as Error).message).not.toContain('secret-password');
|
||||
expect((error as Error).message).not.toContain('secret');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user