feat: define secure static hosting contract

This commit is contained in:
2026-09-02 09:44:15 +03:00
parent 672ba80d9b
commit 847e6e4da9
8 changed files with 638 additions and 27 deletions

View File

@@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest';
import {
buildContentSecurityPolicy,
createSecurityHeaders,
serializeStaticHostHeaders,
} from '@/security/security-policy';
import type { SiteConfig } from '@/content/types';
const jsonLdHash = 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
const productionSite: SiteConfig = {
environment: 'production',
canonicalUrl: 'https://portfolio.example.test/',
buildId: 'release-1',
};
const previewSite: SiteConfig = {
environment: 'preview',
canonicalUrl: 'https://portfolio.localhost/',
buildId: 'preview-1',
};
describe('static-host security policy', () => {
it('builds the exact restrictive CSP around the serialized JSON-LD hash', () => {
expect(buildContentSecurityPolicy(jsonLdHash)).toBe(
[
"default-src 'self'",
`script-src 'self' '${jsonLdHash}'`,
"style-src 'self'",
"img-src 'self' data:",
"media-src 'self'",
"font-src 'self'",
"connect-src 'none'",
"object-src 'none'",
"base-uri 'none'",
"frame-ancestors 'none'",
"form-action 'self'",
'upgrade-insecure-requests',
].join('; '),
);
});
it('emits production security and revalidation headers without widening HSTS', () => {
const headers = createSecurityHeaders(productionSite, jsonLdHash);
expect(headers).toEqual({
csp: buildContentSecurityPolicy(jsonLdHash),
contentTypeOptions: 'nosniff',
referrerPolicy: 'no-referrer',
permissionsPolicy:
'camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()',
frameOptions: 'DENY',
cacheControl: 'public, max-age=0, must-revalidate',
hsts: 'max-age=31536000',
});
expect(headers.hsts).not.toContain('includeSubDomains');
expect(headers).not.toHaveProperty('xRobotsTag');
});
it('keeps preview responses private, noindex, and outside HSTS policy', () => {
const headers = createSecurityHeaders(previewSite, jsonLdHash);
expect(headers.cacheControl).toBe('private, no-store');
expect(headers.xRobotsTag).toBe('noindex, nofollow, noarchive');
expect(headers).not.toHaveProperty('hsts');
});
it.each([
"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' 'unsafe-inline",
"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' *",
'sha256-not-base64!',
"'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='",
])('rejects an invalid or policy-injecting JSON-LD hash: %s', (value) => {
expect(() => buildContentSecurityPolicy(value)).toThrow(/JSON-LD CSP hash/i);
});
it('serializes one exact response-header template with no wildcard or external source expressions', () => {
const serialized = serializeStaticHostHeaders(productionSite, jsonLdHash);
expect(serialized).toContain(`Content-Security-Policy: ${buildContentSecurityPolicy(jsonLdHash)}`);
expect(serialized).toContain('Strict-Transport-Security: max-age=31536000');
expect(serialized).not.toMatch(/unsafe-inline|unsafe-eval|https?:\/\/|\*\.(?:js|css)/u);
});
});