diff --git a/changelog.md b/changelog.md index cb040d5..35a5186 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,18 @@ # Changelog +## 2025-12-05 - 7.9.0 - feat(typedserver) +Add configurable security headers and default SPA behavior + +Introduce structured security headers support (CSP, HSTS, X-Frame-Options, COOP/COEP/CORP, Permissions-Policy, Referrer-Policy, X-XSS-Protection, etc.) and apply them to responses and OPTIONS preflight. Expose configuration via the server API and document usage. Also update UtilityWebsiteServer defaults (SPA fallback enabled by default) and related docs. + +- Add ISecurityHeaders and IContentSecurityPolicy TypeScript interfaces to configure CSP, HSTS and other security-related headers. +- Implement buildCspHeader to serialize CSP config and applyResponseHeaders to add CORS and all configured security headers to outgoing responses. +- Apply security headers to OPTIONS preflight responses and all other responses by default when securityHeaders option is provided. +- Add securityHeaders option to IServerOptions and wire it through TypedServer and UtilityWebsiteServer constructors. +- Update UtilityWebsiteServer: renamed template to UtilityWebsiteServer, enable SPA fallback by default, expose options (cors, spaFallback, securityHeaders, forceSsl, port, feedMetadata, etc.) and forward them into the TypedServer instance. +- Documentation: add Security Headers section and example usage to readme.md; document the UtilityWebsiteServer defaults and example. +- Ensure CORS headers are only added when cors option is enabled. + ## 2025-12-05 - 7.8.18 - fix(readme) Update README to reflect new features and updated examples (SPA/PWA/Edge/ServiceWorker) and clarify API usage diff --git a/readme.md b/readme.md index 54c3861..1a1487b 100644 --- a/readme.md +++ b/readme.md @@ -9,12 +9,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community ## ✨ Features - πŸ”’ **Type-Safe API** - Full TypeScript support with `@api.global/typedrequest` and `@api.global/typedsocket` +- πŸ›‘οΈ **Security Headers** - Built-in CSP, HSTS, X-Frame-Options, and more - ⚑ **Live Reload** - Automatic browser refresh on file changes during development - πŸ› οΈ **Service Worker** - Advanced caching, offline support, and background sync - ☁️ **Edge Workers** - Cloudflare Workers compatible edge computing with domain routing - πŸ“‘ **WebSocket** - Real-time bidirectional communication via TypedSocket - πŸ—ΊοΈ **SEO Tools** - Built-in sitemap, RSS feed, and robots.txt generation -- 🎯 **SPA Support** - Single-page application fallback routing +- 🎯 **SPA Support** - Single-page application fallback routing (default in UtilityWebsiteServer) - πŸ“± **PWA Ready** - Web App Manifest generation for progressive web apps ## πŸ“¦ Installation @@ -189,6 +190,73 @@ const swClient = await getServiceworkerClient({ // - Background sync ``` +## πŸ›‘οΈ Security Headers + +Configure comprehensive security headers including CSP, HSTS, and more: + +```typescript +import { TypedServer } from '@api.global/typedserver'; + +const server = new TypedServer({ + serveDir: './dist', + cors: true, + + securityHeaders: { + // Content Security Policy + csp: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.example.com'], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", 'data:', 'https:'], + connectSrc: ["'self'", 'wss:', 'https://api.example.com'], + fontSrc: ["'self'", 'https://fonts.gstatic.com'], + frameAncestors: ["'none'"], + upgradeInsecureRequests: true, + }, + + // HSTS (HTTP Strict Transport Security) + hstsMaxAge: 31536000, // 1 year + hstsIncludeSubDomains: true, + hstsPreload: true, + + // Other security headers + xFrameOptions: 'DENY', + xContentTypeOptions: true, + xXssProtection: true, + referrerPolicy: 'strict-origin-when-cross-origin', + + // Cross-Origin policies + crossOriginOpenerPolicy: 'same-origin', + crossOriginEmbedderPolicy: 'require-corp', + crossOriginResourcePolicy: 'same-origin', + + // Permissions Policy + permissionsPolicy: { + camera: [], + microphone: [], + geolocation: ['self'], + }, + }, +}); + +await server.start(); +``` + +### Security Headers Reference + +| Header | Option | Description | +|--------|--------|-------------| +| `Content-Security-Policy` | `csp` | Controls resources the browser can load | +| `Strict-Transport-Security` | `hstsMaxAge`, `hstsIncludeSubDomains`, `hstsPreload` | Forces HTTPS connections | +| `X-Frame-Options` | `xFrameOptions` | Prevents clickjacking attacks | +| `X-Content-Type-Options` | `xContentTypeOptions` | Prevents MIME-sniffing | +| `X-XSS-Protection` | `xXssProtection` | Legacy XSS filter (still useful) | +| `Referrer-Policy` | `referrerPolicy` | Controls referrer information | +| `Permissions-Policy` | `permissionsPolicy` | Controls browser features | +| `Cross-Origin-Opener-Policy` | `crossOriginOpenerPolicy` | Isolates browsing context | +| `Cross-Origin-Embedder-Policy` | `crossOriginEmbedderPolicy` | Controls cross-origin embedding | +| `Cross-Origin-Resource-Policy` | `crossOriginResourcePolicy` | Controls cross-origin resource sharing | + ## πŸ“‹ Configuration Reference ### IServerOptions @@ -213,6 +281,7 @@ const swClient = await getServiceworkerClient({ | `defaultAnswer` | `function` | - | Custom default response handler | | `feedMetadata` | `object` | - | RSS feed metadata options | | `blockWaybackMachine` | `boolean` | `false` | Block Wayback Machine archiving | +| `securityHeaders` | `ISecurityHeaders` | - | Security headers configuration (CSP, HSTS, etc.) | ## πŸ—οΈ Package Exports @@ -228,21 +297,57 @@ const swClient = await getServiceworkerClient({ ## πŸ”„ Utility Servers -Pre-configured server templates for common use cases: +Pre-configured server templates with best practices built-in: + +### UtilityWebsiteServer + +Optimized for modern web applications with SPA support enabled by default: ```typescript import { utilityservers } from '@api.global/typedserver'; -// WebsiteServer - optimized for static websites -const websiteServer = new utilityservers.WebsiteServer({ +const websiteServer = new utilityservers.UtilityWebsiteServer({ serveDir: './dist', domain: 'example.com', + + // SPA fallback enabled by default (serves index.html for client routes) + spaFallback: true, // default: true + + // Optional security headers + securityHeaders: { + csp: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"], + styleSrc: ["'self'", "'unsafe-inline'"], + }, + xFrameOptions: 'SAMEORIGIN', + xContentTypeOptions: true, + }, + + // Other options + cors: true, // default: true + forceSsl: false, // default: false + appSemVer: '1.0.0', }); -// ServiceServer - optimized for API services -const serviceServer = new utilityservers.ServiceServer({ - cors: true, +await websiteServer.start(); // Default port 3000 +``` + +### UtilityServiceServer + +Optimized for API services: + +```typescript +import { utilityservers } from '@api.global/typedserver'; + +const serviceServer = new utilityservers.UtilityServiceServer({ + serviceName: 'My API', + serviceVersion: '1.0.0', + serviceDomain: 'api.example.com', + port: 8080, }); + +await serviceServer.start(); ``` ## License and Legal Information diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index fc6bb76..e8c80bc 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@api.global/typedserver', - version: '7.8.18', + version: '7.9.0', description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.' } diff --git a/ts/classes.typedserver.ts b/ts/classes.typedserver.ts index 643c461..4512878 100644 --- a/ts/classes.typedserver.ts +++ b/ts/classes.typedserver.ts @@ -1,10 +1,80 @@ import * as plugins from './plugins.js'; -import * as paths from './paths.js'; import * as interfaces from '../dist_ts_interfaces/index.js'; import { DevToolsController } from './controllers/controller.devtools.js'; import { TypedRequestController } from './controllers/controller.typedrequest.js'; import { BuiltInRoutesController } from './controllers/controller.builtin.js'; +/** + * Content Security Policy configuration + * Each directive can be a string or array of sources + */ +export interface IContentSecurityPolicy { + /** Fallback for other directives */ + defaultSrc?: string | string[]; + /** Valid sources for scripts */ + scriptSrc?: string | string[]; + /** Valid sources for stylesheets */ + styleSrc?: string | string[]; + /** Valid sources for images */ + imgSrc?: string | string[]; + /** Valid sources for fonts */ + fontSrc?: string | string[]; + /** Valid sources for AJAX, WebSockets, etc. */ + connectSrc?: string | string[]; + /** Valid sources for media (audio/video) */ + mediaSrc?: string | string[]; + /** Valid sources for frames */ + frameSrc?: string | string[]; + /** Valid sources for , , */ + objectSrc?: string | string[]; + /** Valid sources for web workers */ + workerSrc?: string | string[]; + /** Valid sources for form actions */ + formAction?: string | string[]; + /** Controls which URLs can embed the page */ + frameAncestors?: string | string[]; + /** Restricts URLs for element */ + baseUri?: string | string[]; + /** Report violations to this URL */ + reportUri?: string; + /** Report violations to this endpoint */ + reportTo?: string; + /** Upgrade insecure requests to HTTPS */ + upgradeInsecureRequests?: boolean; + /** Block all mixed content */ + blockAllMixedContent?: boolean; +} + +/** + * Security headers configuration + */ +export interface ISecurityHeaders { + /** Content Security Policy */ + csp?: IContentSecurityPolicy; + /** X-Frame-Options: DENY, SAMEORIGIN, or ALLOW-FROM uri */ + xFrameOptions?: 'DENY' | 'SAMEORIGIN' | string; + /** X-Content-Type-Options: nosniff */ + xContentTypeOptions?: boolean; + /** X-XSS-Protection header (legacy, but still useful) */ + xXssProtection?: boolean | string; + /** Referrer-Policy header */ + referrerPolicy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'; + /** Strict-Transport-Security (HSTS) max-age in seconds */ + hstsMaxAge?: number; + /** Include subdomains in HSTS */ + hstsIncludeSubDomains?: boolean; + /** HSTS preload flag */ + hstsPreload?: boolean; + /** Permissions-Policy (formerly Feature-Policy) */ + permissionsPolicy?: Record; + /** Cross-Origin-Opener-Policy */ + crossOriginOpenerPolicy?: 'unsafe-none' | 'same-origin-allow-popups' | 'same-origin'; + /** Cross-Origin-Embedder-Policy */ + crossOriginEmbedderPolicy?: 'unsafe-none' | 'require-corp' | 'credentialless'; + /** Cross-Origin-Resource-Policy */ + crossOriginResourcePolicy?: 'same-site' | 'same-origin' | 'cross-origin'; +} + export interface IServerOptions { /** * serve a particular directory @@ -62,6 +132,11 @@ export interface IServerOptions { * Useful for single-page applications with client-side routing */ spaFallback?: boolean; + + /** + * Security headers configuration (CSP, HSTS, X-Frame-Options, etc.) + */ + securityHeaders?: ISecurityHeaders; } export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'ALL'; @@ -388,16 +463,133 @@ export class TypedServer { } /** - * Add CORS headers to a response + * Build CSP header string from configuration */ - private addCorsHeaders(response: Response): Response { - if (!this.options.cors) return response; + private buildCspHeader(csp: IContentSecurityPolicy): string { + const directives: string[] = []; + const addDirective = (name: string, value: string | string[] | undefined) => { + if (value) { + const sources = Array.isArray(value) ? value.join(' ') : value; + directives.push(`${name} ${sources}`); + } + }; + + addDirective('default-src', csp.defaultSrc); + addDirective('script-src', csp.scriptSrc); + addDirective('style-src', csp.styleSrc); + addDirective('img-src', csp.imgSrc); + addDirective('font-src', csp.fontSrc); + addDirective('connect-src', csp.connectSrc); + addDirective('media-src', csp.mediaSrc); + addDirective('frame-src', csp.frameSrc); + addDirective('object-src', csp.objectSrc); + addDirective('worker-src', csp.workerSrc); + addDirective('form-action', csp.formAction); + addDirective('frame-ancestors', csp.frameAncestors); + addDirective('base-uri', csp.baseUri); + + if (csp.reportUri) { + directives.push(`report-uri ${csp.reportUri}`); + } + if (csp.reportTo) { + directives.push(`report-to ${csp.reportTo}`); + } + if (csp.upgradeInsecureRequests) { + directives.push('upgrade-insecure-requests'); + } + if (csp.blockAllMixedContent) { + directives.push('block-all-mixed-content'); + } + + return directives.join('; '); + } + + /** + * Apply all configured headers (CORS, security) to a response + */ + private applyResponseHeaders(response: Response): Response { const headers = new Headers(response.headers); - headers.set('Access-Control-Allow-Origin', '*'); - headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH'); - headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); - headers.set('Access-Control-Max-Age', '86400'); + + // CORS headers + if (this.options.cors) { + headers.set('Access-Control-Allow-Origin', '*'); + headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH'); + headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); + headers.set('Access-Control-Max-Age', '86400'); + } + + // Security headers + const security = this.options.securityHeaders; + if (security) { + // Content Security Policy + if (security.csp) { + const cspHeader = this.buildCspHeader(security.csp); + if (cspHeader) { + headers.set('Content-Security-Policy', cspHeader); + } + } + + // X-Frame-Options + if (security.xFrameOptions) { + headers.set('X-Frame-Options', security.xFrameOptions); + } + + // X-Content-Type-Options + if (security.xContentTypeOptions) { + headers.set('X-Content-Type-Options', 'nosniff'); + } + + // X-XSS-Protection + if (security.xXssProtection) { + const value = typeof security.xXssProtection === 'string' + ? security.xXssProtection + : '1; mode=block'; + headers.set('X-XSS-Protection', value); + } + + // Referrer-Policy + if (security.referrerPolicy) { + headers.set('Referrer-Policy', security.referrerPolicy); + } + + // Strict-Transport-Security (HSTS) + if (security.hstsMaxAge !== undefined) { + let hsts = `max-age=${security.hstsMaxAge}`; + if (security.hstsIncludeSubDomains) { + hsts += '; includeSubDomains'; + } + if (security.hstsPreload) { + hsts += '; preload'; + } + headers.set('Strict-Transport-Security', hsts); + } + + // Permissions-Policy + if (security.permissionsPolicy) { + const policies = Object.entries(security.permissionsPolicy) + .map(([feature, allowlist]) => `${feature}=(${allowlist.join(' ')})`) + .join(', '); + if (policies) { + headers.set('Permissions-Policy', policies); + } + } + + // Cross-Origin-Opener-Policy + if (security.crossOriginOpenerPolicy) { + headers.set('Cross-Origin-Opener-Policy', security.crossOriginOpenerPolicy); + } + + // Cross-Origin-Embedder-Policy + if (security.crossOriginEmbedderPolicy) { + headers.set('Cross-Origin-Embedder-Policy', security.crossOriginEmbedderPolicy); + } + + // Cross-Origin-Resource-Policy + if (security.crossOriginResourcePolicy) { + headers.set('Cross-Origin-Resource-Policy', security.crossOriginResourcePolicy); + } + } return new Response(response.body, { status: response.status, @@ -416,12 +608,12 @@ export class TypedServer { // Handle OPTIONS preflight for CORS if (method === 'OPTIONS' && this.options.cors) { - return this.addCorsHeaders(new Response(null, { status: 204 })); + return this.applyResponseHeaders(new Response(null, { status: 204 })); } - // Process the request and wrap response with CORS headers + // Process the request and wrap response with all configured headers const response = await this.handleRequestInternal(request, url, path, method); - return this.addCorsHeaders(response); + return this.applyResponseHeaders(response); } /** diff --git a/ts/utilityservers/classes.websiteserver.ts b/ts/utilityservers/classes.websiteserver.ts index ac7c2f4..b8ff306 100644 --- a/ts/utilityservers/classes.websiteserver.ts +++ b/ts/utilityservers/classes.websiteserver.ts @@ -1,13 +1,28 @@ import * as interfaces from '../../dist_ts_interfaces/index.js'; -import { type IServerOptions, TypedServer } from '../classes.typedserver.js'; +import { type IServerOptions, type ISecurityHeaders, TypedServer } from '../classes.typedserver.js'; import * as plugins from '../plugins.js'; export interface IUtilityWebsiteServerConstructorOptions { + /** Custom route handler to add additional routes */ addCustomRoutes?: (typedserver: TypedServer) => Promise; + /** Application semantic version */ appSemVer?: string; + /** Domain name for the website */ domain: string; + /** Directory to serve static files from */ serveDir: string; - feedMetadata: IServerOptions['feedMetadata']; + /** RSS feed metadata */ + feedMetadata?: IServerOptions['feedMetadata']; + /** Enable/disable CORS (default: true) */ + cors?: boolean; + /** Enable/disable SPA fallback (default: true) */ + spaFallback?: boolean; + /** Security headers configuration */ + securityHeaders?: ISecurityHeaders; + /** Force SSL redirect (default: false) */ + forceSsl?: boolean; + /** Port to listen on (default: 3000) */ + port?: number; } /** @@ -29,14 +44,28 @@ export class UtilityWebsiteServer { /** * Start the website server */ - public async start(portArg = 3000) { + public async start(portArg?: number) { + const port = portArg ?? this.options.port ?? 3000; + this.typedserver = new TypedServer({ - cors: true, - injectReload: true, - watch: true, + // Core settings + cors: this.options.cors ?? true, serveDir: this.options.serveDir, domain: this.options.domain, - forceSsl: false, + port, + + // Development features + injectReload: true, + watch: true, + + // SPA support (enabled by default for modern web apps) + spaFallback: this.options.spaFallback ?? true, + + // Security + forceSsl: this.options.forceSsl ?? false, + securityHeaders: this.options.securityHeaders, + + // PWA manifest manifest: { name: this.options.domain, short_name: this.options.domain, @@ -46,11 +75,11 @@ export class UtilityWebsiteServer { background_color: '#000000', scope: '/', }, - port: portArg, - // features + // SEO features robots: true, sitemap: true, + feedMetadata: this.options.feedMetadata, }); let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] = {