const fs = require('fs');
const path = require('path');
const request = require('supertest');
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
/**
* Mirrors what a production `client/dist/index.html` actually contains: inline
* style, inline script, a module entry, and the module preloads Vite emits.
*/
const INDEX_HTML =
'
LibreChat' +
'' +
'' +
'' +
'' +
'' +
'' +
'';
jest.mock('~/server/services/Config', () => ({
syncStaticTools: jest.fn().mockResolvedValue(undefined),
mergeAppTools: jest.fn().mockResolvedValue(undefined),
loadCustomConfig: jest.fn(() => Promise.resolve({})),
getAppConfig: jest.fn().mockResolvedValue({
paths: {
uploads: '/tmp',
dist: '/tmp/dist-csp',
fonts: '/tmp/fonts-csp',
assets: '/tmp/assets-csp',
},
fileStrategy: 'local',
imageOutputType: 'PNG',
}),
setCachedTools: jest.fn(),
}));
jest.mock('~/server/services/Agents/triggers', () => ({
initializeAgentTriggerService: jest.fn().mockResolvedValue(undefined),
}));
jest.mock('~/server/services/Schedules', () => ({
initializeScheduleEngine: jest.fn().mockResolvedValue(undefined),
}));
jest.mock('~/app/clients/tools', () => ({
createOpenAIImageTools: jest.fn(() => []),
createYouTubeTools: jest.fn(() => []),
manifestToolMap: {},
toolkits: [],
}));
jest.mock('~/config', () => ({
createMCPServersRegistry: jest.fn(),
createMCPManager: jest.fn().mockResolvedValue({
getAppToolFunctions: jest.fn().mockResolvedValue({}),
}),
}));
jest.mock(
'@librechat/api/telemetry',
() => ({
initializeTelemetry: jest.fn(() => ({
enabled: false,
status: 'disabled',
shutdown: jest.fn(),
})),
telemetryMiddleware: jest.fn((_req, _res, next) => next()),
telemetryErrorMiddleware: jest.fn((err, _req, _res, next) => next(err)),
}),
{ virtual: true },
);
describe('Content Security Policy', () => {
jest.setTimeout(30_000);
let mongoServer;
let app;
const originalReadFileSync = fs.readFileSync;
beforeAll(async () => {
fs.readFileSync = function (filepath, options) {
if (filepath.includes('index.html')) {
return INDEX_HTML;
}
return originalReadFileSync(filepath, options);
};
for (const dir of ['/tmp/dist-csp', '/tmp/fonts-csp', '/tmp/assets-csp']) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
fs.writeFileSync(path.join('/tmp/dist-csp', 'index.html'), INDEX_HTML);
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URI = mongoServer.getUri();
process.env.PORT = '0';
/* Read once at startup, so they must be set before the server module loads. */
process.env.CSP_ENABLED = 'true';
process.env.CSP_REPORT_ONLY = 'false';
process.env.CSP_CONNECT_SRC_EXTRA = 'https://telemetry.example.com';
/* A cacheable override that CSP must refuse for the shell. */
process.env.INDEX_CACHE_CONTROL = 'public, max-age=3600';
app = require('~/server');
await healthCheckPoll(app);
});
afterAll(async () => {
fs.readFileSync = originalReadFileSync;
delete process.env.CSP_ENABLED;
delete process.env.CSP_REPORT_ONLY;
delete process.env.CSP_CONNECT_SRC_EXTRA;
delete process.env.INDEX_CACHE_CONTROL;
await mongoServer.stop();
await mongoose.disconnect();
});
it('sends an enforcing policy whose nonce matches the served scripts', async () => {
const response = await request(app).get('/');
const csp = response.headers['content-security-policy'];
const nonce = csp?.match(/script-src 'nonce-([^']+)'/)?.[1];
expect(response.status).toBe(200);
expect(response.headers['content-security-policy-report-only']).toBeUndefined();
expect(nonce).toBeTruthy();
expect(response.text).toContain(``);
expect(response.text).toContain(`