fix(server): allow google translate origin in content security policy

Add translate.googleapis.com to the connect-src Content-Security-Policy directive so the built-in eBook reader's translation feature can perform fetch requests without being blocked by CSP.

Closes #786
This commit is contained in:
neon
2026-07-26 19:17:33 +00:00
parent 3c6e8ed0d4
commit 9b2430a47e
2 changed files with 39 additions and 0 deletions
@@ -179,6 +179,12 @@ describe('buildCspDirectives', () => {
expect(connectSrc).toContain('https://api.dictionaryapi.dev');
expect(connectSrc).toContain('https://*.wiktionary.org');
});
it('includes the Google Translate endpoint used by the epub reader regardless of Cloudflare Insights setting', () => {
expect(buildCspDirectives({ allowCloudflareInsights: false }).connectSrc).toContain('https://translate.googleapis.com');
expect(buildCspDirectives({ allowCloudflareInsights: true }).connectSrc).toContain('https://translate.googleapis.com');
expect(buildCspDirectives().connectSrc).toContain('https://translate.googleapis.com');
});
});
describe('scriptSrc with Cloudflare Insights', () => {
@@ -249,6 +255,24 @@ describe('buildCspDirectives', () => {
expect(upgradeInsecureRequests).toBeNull();
});
});
describe('buildHelmetOptions (integration)', () => {
it('emits a Content-Security-Policy header whose connect-src allows the Google Translate endpoint', async () => {
await withCspApp(async (app) => {
const response = await app.inject({ method: 'GET', url: '/ping' });
const csp = response.headers['content-security-policy'];
expect(csp).toBeDefined();
const connectSrcDirective = String(csp)
.split(';')
.map((directive) => directive.trim())
.find((directive) => directive.startsWith('connect-src'));
expect(connectSrcDirective).toContain('https://translate.googleapis.com');
});
});
});
});
describe('empty request body bootstrap handling', () => {
@@ -463,6 +487,19 @@ async function withHstsApp(options: { trustProxy: boolean }, run: (app: FastifyI
}
}
async function withCspApp(run: (app: FastifyInstance) => Promise<void>): Promise<void> {
const app = Fastify({ logger: false });
await app.register(fastifyHelmet, buildHelmetOptions());
app.get('/ping', () => ({ ok: true }));
try {
await app.ready();
await run(app);
} finally {
await app.close();
}
}
async function withParserApp(run: (app: FastifyInstance) => Promise<void>): Promise<void> {
const app = Fastify({ logger: false });
registerEmptyBodyContentTypeParser(app);
@@ -41,6 +41,7 @@ export interface CspOptions {
const CLOUDFLARE_INSIGHTS_SCRIPT_SRC = 'https://static.cloudflareinsights.com';
const CLOUDFLARE_INSIGHTS_CONNECT_SRC = 'https://cloudflareinsights.com';
const DICTIONARY_CONNECT_SRC = ['https://api.dictionaryapi.dev', 'https://*.wiktionary.org'];
const TRANSLATE_CONNECT_SRC = 'https://translate.googleapis.com';
export function buildCspDirectives(options: CspOptions = {}) {
const { allowCloudflareInsights = false } = options;
@@ -52,6 +53,7 @@ export function buildCspDirectives(options: CspOptions = {}) {
'wss:',
'https://cdn.jsdelivr.net',
...DICTIONARY_CONNECT_SRC,
TRANSLATE_CONNECT_SRC,
...(allowCloudflareInsights ? [CLOUDFLARE_INSIGHTS_CONNECT_SRC] : []),
];