mirror of
https://github.com/thedotmack/claude-mem.git
synced 2026-09-20 04:23:02 +08:00
b45515c5fe
- #2572: server keys/jobs/api-key migrate-scopes CLI subcommands (secrets never printed), hand-rolled security headers (no helmet dep), wrong-runtime guard. - #2552: mount viewer static handler + compat API on the server runtime (ServerViewerRoutes). - #2554: fix stale Claude model (claude-3-5-sonnet-latest -> claude-sonnet-4-6); document subscription vs API-key auth; confirm 0.0.0.0 bind avoids loopback ECONNREFUSED. - #2558: docker-compose restart: unless-stopped on all services, REDIS_URL fallback, credentials-file mount (config-only, not runtime-verified in sandbox). - #2560: postgres platform_source column+indexes (idempotent), thread platform_source end-to-end through events schema/storage/routes/compat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// #2572 — the server runtime must emit hardening response headers. The worker
|
|
// (loopback-only) leaves them off. We assert the opt-in `securityHeaders`
|
|
// option installs the headers on every response and is absent by default.
|
|
|
|
import { afterEach, describe, expect, it, spyOn } from 'bun:test';
|
|
import { logger } from '../../src/utils/logger.js';
|
|
import { Server, type ServerOptions } from '../../src/services/server/Server.js';
|
|
|
|
function baseOptions(overrides: Partial<ServerOptions> = {}): ServerOptions {
|
|
return {
|
|
getInitializationComplete: () => true,
|
|
getMcpReady: () => true,
|
|
onShutdown: () => Promise.resolve(),
|
|
onRestart: () => Promise.resolve(),
|
|
workerPath: '/test/worker-service.cjs',
|
|
getAiStatus: () => ({ provider: 'disabled', authMethod: 'api-key', lastInteraction: null }),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('Server security headers (#2572)', () => {
|
|
let server: Server | null = null;
|
|
let spies: ReturnType<typeof spyOn>[] = [];
|
|
|
|
afterEach(async () => {
|
|
spies.forEach(s => s.mockRestore());
|
|
spies = [];
|
|
if (server?.getHttpServer()) {
|
|
try { await server.close(); } catch { /* ignore */ }
|
|
}
|
|
server = null;
|
|
});
|
|
|
|
it('emits hardening headers on a server response when securityHeaders=true', async () => {
|
|
spies = [spyOn(logger, 'info').mockImplementation(() => {})];
|
|
server = new Server(baseOptions({ securityHeaders: true }));
|
|
const port = 41000 + Math.floor(Math.random() * 9000);
|
|
await server.listen(port, '127.0.0.1');
|
|
|
|
const res = await fetch(`http://127.0.0.1:${port}/api/health`);
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get('x-content-type-options')).toBe('nosniff');
|
|
expect(res.headers.get('x-frame-options')).toBe('DENY');
|
|
expect(res.headers.get('referrer-policy')).toBe('no-referrer');
|
|
expect(res.headers.get('cross-origin-opener-policy')).toBe('same-origin');
|
|
expect(res.headers.get('x-powered-by')).toBeNull();
|
|
});
|
|
|
|
it('does NOT emit the hardening headers by default (worker runtime)', async () => {
|
|
spies = [spyOn(logger, 'info').mockImplementation(() => {})];
|
|
server = new Server(baseOptions());
|
|
const port = 41000 + Math.floor(Math.random() * 9000);
|
|
await server.listen(port, '127.0.0.1');
|
|
|
|
const res = await fetch(`http://127.0.0.1:${port}/api/health`);
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get('x-content-type-options')).toBeNull();
|
|
expect(res.headers.get('x-frame-options')).toBeNull();
|
|
});
|
|
});
|