First Init
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped

This commit is contained in:
2026-03-21 16:46:46 +05:30
commit d402256547
216 changed files with 48375 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
import { defineMiddleware } from 'astro:middleware';
import { logger } from './utils/logger';
import { captureException } from './utils/sentry';
/**
* Security Middleware
* Implements security headers and domain validation for workroot.in
*
* Security Headers Implemented:
* - Content-Security-Policy (CSP): Controls resource loading
* - X-Frame-Options: Prevents clickjacking
* - X-Content-Type-Options: Prevents MIME sniffing
* - Referrer-Policy: Controls referrer information
* - Permissions-Policy: Controls browser features
* - Strict-Transport-Security (HSTS): Enforces HTTPS
*/
// CSRF protection: validate Origin/Referer for state-changing API requests
function validateCsrfOrigin(request: Request, url: URL): boolean {
const method = request.method;
const path = url.pathname;
// Only enforce on state-changing API methods
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) return true;
if (!path.startsWith('/api/')) return true;
// In development, allow any origin
if (!import.meta.env.PROD) return true;
const origin = request.headers.get('origin');
const referer = request.headers.get('referer');
if (origin) {
return origin === 'https://workroot.in' || origin === 'https://www.workroot.in';
}
if (referer) {
return referer.startsWith('https://workroot.in') || referer.startsWith('https://www.workroot.in');
}
// If neither origin nor referer is present, reject (missing headers = suspicious)
return false;
}
export const onRequest = defineMiddleware(async (context, next) => {
const startTime = Date.now();
const method = context.request.method;
const path = context.url.pathname;
// CSRF origin validation for API mutations
if (!validateCsrfOrigin(context.request, context.url)) {
logger.warn('middleware', 'CSRF origin check failed', {
method,
path,
origin: context.request.headers.get('origin'),
referer: context.request.headers.get('referer'),
});
return new Response(
JSON.stringify({ success: false, error: 'Forbidden: invalid origin.' }),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
);
}
let response: Response;
try {
response = await next();
} catch (err) {
const durationMs = Date.now() - startTime;
await captureException(err, {
scope: 'middleware',
extra: { method, path, durationMs },
});
// Re-throw so Astro can render its 500 page
throw err;
}
const durationMs = Date.now() - startTime;
const status = response.status;
// Log all SSR errors (5xx) and not-found (404) responses
if (status >= 500) {
logger.error('middleware', `SSR error ${status}`, { method, path, status, durationMs });
} else if (status === 404) {
logger.warn('middleware', `Not found`, { method, path, status, durationMs });
} else if (status >= 400) {
logger.warn('middleware', `Client error ${status}`, { method, path, status, durationMs });
} else {
logger.debug('middleware', `Request`, { method, path, status, durationMs });
}
// Clone the response to add headers
const modifiedResponse = new Response(response.body, response);
// Content Security Policy (CSP)
// Allows:
// - self: Same-origin resources
// - https://images.unsplash.com: External images (explicit, not wildcard https:)
// - https://fonts.googleapis.com & https://fonts.gstatic.com: Google Fonts
// - inline scripts/styles: Required for Astro framework
// - https://www.googletagmanager.com & https://www.google-analytics.com: GA4
// - https://plausible.io: Plausible Analytics
// Note: unsafe-eval is removed in production (only needed during Astro HMR dev mode)
const isProduction = import.meta.env.PROD;
const scriptSrc = isProduction
? "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io"
: "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io";
const csp = [
"default-src 'self'",
scriptSrc,
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https://images.unsplash.com https://www.google-analytics.com https://www.googletagmanager.com",
"font-src 'self' data: https://fonts.gstatic.com",
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net https://plausible.io",
"worker-src 'self'",
"manifest-src 'self'",
"object-src 'none'", // Block Flash/plugins (defense-in-depth)
"frame-src 'none'", // No embedded frames allowed
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
].join('; ');
modifiedResponse.headers.set('Content-Security-Policy', csp);
// X-Frame-Options: Prevent clickjacking
// DENY prevents any domain from framing the content
modifiedResponse.headers.set('X-Frame-Options', 'DENY');
// X-Content-Type-Options: Prevent MIME sniffing
// nosniff prevents browsers from interpreting files as different MIME type
modifiedResponse.headers.set('X-Content-Type-Options', 'nosniff');
// Referrer-Policy: Control referrer information
// strict-origin-when-cross-origin sends full URL for same-origin, origin only for cross-origin
modifiedResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions-Policy: Disable unnecessary browser features
// Restricts access to sensitive APIs to prevent abuse
modifiedResponse.headers.set('Permissions-Policy',
'geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()'
);
// Strict-Transport-Security (HSTS): Enforce HTTPS
// max-age=31536000: 1 year
// includeSubDomains: Apply to all subdomains
// preload: Allow inclusion in HSTS preload list
// Only set HSTS in production and when using HTTPS
const isHttps = context.url.protocol === 'https:';
if (isProduction && isHttps) {
modifiedResponse.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
}
// X-Permitted-Cross-Domain-Policies: Restrict cross-domain access
modifiedResponse.headers.set('X-Permitted-Cross-Domain-Policies', 'none');
// X-DNS-Prefetch-Control: Control DNS prefetching
modifiedResponse.headers.set('X-DNS-Prefetch-Control', 'on');
// Domain validation: Ensure requests are for workroot.in
const allowedHosts = ['workroot.in', 'www.workroot.in', 'localhost', '127.0.0.1', '0.0.0.0'];
const requestHost = context.url.hostname;
// In development, allow any host
if (!isProduction || allowedHosts.includes(requestHost)) {
return modifiedResponse;
}
// In production, if host doesn't match, redirect to canonical domain
if (isProduction && !allowedHosts.includes(requestHost)) {
const canonicalUrl = `https://workroot.in${context.url.pathname}${context.url.search}`;
return Response.redirect(canonicalUrl, 301);
}
return modifiedResponse;
});