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.
156 lines
4.9 KiB
TypeScript
156 lines
4.9 KiB
TypeScript
import * as interfaces from '../../dist_ts_interfaces/index.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<any>;
|
|
/** Application semantic version */
|
|
appSemVer?: string;
|
|
/** Domain name for the website */
|
|
domain: string;
|
|
/** Directory to serve static files from */
|
|
serveDir: string;
|
|
/** 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;
|
|
}
|
|
|
|
/**
|
|
* the utility website server implements a best practice server for websites
|
|
* It supports:
|
|
* * live reload
|
|
* * serviceworker
|
|
* * pwa manifest
|
|
*/
|
|
export class UtilityWebsiteServer {
|
|
public options: IUtilityWebsiteServerConstructorOptions;
|
|
public typedserver: TypedServer;
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
|
|
constructor(optionsArg: IUtilityWebsiteServerConstructorOptions) {
|
|
this.options = optionsArg;
|
|
}
|
|
|
|
/**
|
|
* Start the website server
|
|
*/
|
|
public async start(portArg?: number) {
|
|
const port = portArg ?? this.options.port ?? 3000;
|
|
|
|
this.typedserver = new TypedServer({
|
|
// Core settings
|
|
cors: this.options.cors ?? true,
|
|
serveDir: this.options.serveDir,
|
|
domain: this.options.domain,
|
|
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,
|
|
start_url: '/',
|
|
display_override: ['window-controls-overlay'],
|
|
lang: 'en',
|
|
background_color: '#000000',
|
|
scope: '/',
|
|
},
|
|
|
|
// SEO features
|
|
robots: true,
|
|
sitemap: true,
|
|
feedMetadata: this.options.feedMetadata,
|
|
});
|
|
|
|
let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] = {
|
|
appHash: 'xxxxxx',
|
|
appSemVer: this.options.appSemVer || 'x.x.x',
|
|
};
|
|
|
|
// -> Service worker version info handler
|
|
this.typedserver.typedrouter.addTypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
|
|
new plugins.typedrequest.TypedHandler('serviceworker_versionInfo', async () => {
|
|
return lswData;
|
|
})
|
|
);
|
|
|
|
// ads.txt handler
|
|
this.typedserver.addRoute('/ads.txt', 'GET', async () => {
|
|
const adsTxt =
|
|
['google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0'].join('\n') + '\n';
|
|
return new Response(adsTxt, {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'text/plain' },
|
|
});
|
|
});
|
|
|
|
// Asset broker manifest handler
|
|
this.typedserver.addRoute(
|
|
'/assetbroker/manifest/:manifestAsset',
|
|
'GET',
|
|
async (request: Request) => {
|
|
let manifestAssetName = (request as any).params?.manifestAsset;
|
|
if (manifestAssetName === 'favicon.png') {
|
|
manifestAssetName = `favicon_${this.options.domain
|
|
.replace('.', '')
|
|
.replace('losslesscom', 'lossless')}@2x_transparent.png`;
|
|
}
|
|
const fullOriginAssetUrl = `https://assetbroker.lossless.one/brandfiles/00general/${manifestAssetName}`;
|
|
console.log(`Getting ${manifestAssetName} from ${fullOriginAssetUrl}`);
|
|
const smartRequest = plugins.smartrequest.SmartRequest.create();
|
|
const response = await smartRequest.url(fullOriginAssetUrl).get();
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
return new Response(arrayBuffer, {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'image/png' },
|
|
});
|
|
}
|
|
);
|
|
|
|
// Add any custom routes
|
|
if (this.options.addCustomRoutes) {
|
|
await this.options.addCustomRoutes(this.typedserver);
|
|
}
|
|
|
|
// Subscribe to serve directory hash changes
|
|
this.typedserver.serveDirHashSubject.subscribe((appHash: string) => {
|
|
lswData = {
|
|
appHash,
|
|
appSemVer: '1.0.0',
|
|
};
|
|
});
|
|
|
|
// Setup the typedrouter chain
|
|
this.typedserver.typedrouter.addTypedRouter(this.typedrouter);
|
|
|
|
// Start everything
|
|
console.log('routes are all set. Starting up now!');
|
|
await this.typedserver.start();
|
|
console.log('typedserver started!');
|
|
}
|
|
|
|
public async stop() {
|
|
await this.typedserver.stop();
|
|
}
|
|
}
|