Kick off (#1)

* Kick off
* Update LICENSE copyright
This commit is contained in:
2026-04-11 13:22:36 +02:00
committed by GitHub
parent 1f1e74c9f8
commit 3882a1941a
76 changed files with 17154 additions and 1 deletions

297
src/index.ts Normal file
View File

@@ -0,0 +1,297 @@
import { checkAuth } from '../status-page/src/lib/auth.js';
import { handleAggregation } from './aggregation.js';
import { executeDnsCheck } from './checks/dns.js';
import { executeHttpCheck } from './checks/http.js';
import { executeTcpCheck } from './checks/tcp.js';
import { parseConfig } from './config/index.js';
import { prepareChecks } from './checker/index.js';
import { processResults } from './processor/index.js';
import { formatWebhookPayload } from './alert/index.js';
import { getStatusApiData } from './api/status.js';
import { getMonitorStates, writeCheckResults, updateMonitorStates, recordAlert } from './db.js';
import { interpolateSecrets } from './utils/interpolate.js';
import type { Env } from './types.js';
import type { CheckRequest } from './checker/types.js';
import type { CheckResult } from './processor/types.js';
import type { Config } from './config/types.js';
const worker = {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/api/status') {
try {
const configYaml = interpolateSecrets(env.MONITORS_CONFIG, env);
const config = parseConfig(configYaml);
const data = await getStatusApiData(env.DB, config);
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Status API error:', error);
return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
// Auth check for all non-API routes
const authResponse = await checkAuth(request, env);
if (authResponse) {
return authResponse;
}
// Only cache GET requests when status page is public
let cacheKey: Request | undefined;
if (request.method === 'GET' && env.STATUS_PUBLIC === 'true') {
// Create normalized cache key to prevent bypass via query params, headers, or cookies
const normalizedUrl = new URL(url);
normalizedUrl.search = ''; // Remove query parameters
normalizedUrl.hash = ''; // Remove hash fragment
cacheKey = new Request(normalizedUrl.toString());
const cachedResponse = await caches.default.match(cacheKey);
if (cachedResponse) {
console.log(
JSON.stringify({
event: 'cache_hit',
url: url.toString(),
normalizedUrl: normalizedUrl.toString(),
})
);
return cachedResponse;
}
console.log(
JSON.stringify({
event: 'cache_miss',
url: url.toString(),
normalizedUrl: normalizedUrl.toString(),
})
);
}
// Try static assets first (CSS, JS, favicon, etc.)
if (env.ASSETS) {
const assetResponse = await env.ASSETS.fetch(request);
if (assetResponse.status !== 404) {
return assetResponse;
}
}
// Delegate to Astro SSR app for page rendering
try {
const astroMod: { default: ExportedHandler } = await import(
// @ts-expect-error -- build artifact, resolved at bundle time
'../status-page/dist/server/index.mjs'
);
if (astroMod.default.fetch) {
const response = await astroMod.default.fetch(
request as unknown as Request<unknown, IncomingRequestCfProperties>,
env,
ctx
);
// Cache successful responses when status page is public
if (
request.method === 'GET' &&
env.STATUS_PUBLIC === 'true' &&
response.status === 200 &&
cacheKey
) {
const responseWithCache = new Response(response.body, response);
responseWithCache.headers.set('Cache-Control', 'public, max-age=60');
ctx.waitUntil(caches.default.put(cacheKey, responseWithCache.clone()));
return responseWithCache;
}
return response;
}
return new Response('Not Found', { status: 404 });
} catch (error) {
console.error(JSON.stringify({ event: 'astro_ssr_error', error: String(error) }));
return new Response('Internal Server Error', { status: 500 });
}
},
async scheduled(event: ScheduledController, env: Env, _: ExecutionContext): Promise<void> {
if (event.cron === '0 * * * *') {
await handleAggregation(env);
return;
}
try {
const configYaml = interpolateSecrets(env.MONITORS_CONFIG, env);
const config = parseConfig(configYaml);
const checks = prepareChecks(config);
if (checks.length === 0) {
console.warn(JSON.stringify({ event: 'no_monitors_configured' }));
return;
}
const results = await executeAllChecks(checks, env);
const states = await getMonitorStates(env.DB);
const actions = processResults(results, states, config);
await writeCheckResults(env.DB, actions.dbWrites);
await updateMonitorStates(env.DB, actions.stateUpdates);
await Promise.all(
actions.alerts.map(async alert => {
const success = await sendWebhook(alert, config);
await recordAlert(env.DB, alert.monitorName, alert.alertType, alert.alertName, success);
})
);
console.warn(
JSON.stringify({
event: 'scheduled_complete',
checks: checks.length,
alerts: actions.alerts.length,
})
);
} catch (error) {
console.error(
JSON.stringify({
event: 'scheduled_error',
error: error instanceof Error ? error.message : String(error),
})
);
throw error;
}
},
} satisfies ExportedHandler<Env>;
export default worker;
async function executeAllChecks(checks: CheckRequest[], env: Env): Promise<CheckResult[]> {
const promises = checks.map(async check => executeCheck(check, env));
return Promise.all(promises);
}
async function executeCheck(check: CheckRequest, env: Env): Promise<CheckResult> {
// If region is specified and we have Durable Object binding, run check from that region
if (check.region && env.REGIONAL_CHECKER_DO) {
try {
console.warn(
JSON.stringify({ event: 'regional_check_start', monitor: check.name, region: check.region })
);
// Create Durable Object ID from monitor name
const doId = env.REGIONAL_CHECKER_DO.idFromName(check.name);
const doStub = env.REGIONAL_CHECKER_DO.get(doId, {
locationHint: check.region as DurableObjectLocationHint,
});
type RegionalCheckerStub = {
runCheck: (check: CheckRequest) => Promise<CheckResult>;
kill: () => Promise<void>;
};
const typedStub = doStub as unknown as RegionalCheckerStub;
const result = await typedStub.runCheck(check);
// Kill the Durable Object to save resources
try {
await typedStub.kill();
} catch {
// Ignore kill errors - Durable Object will be garbage collected
}
console.warn(
JSON.stringify({
event: 'regional_check_complete',
monitor: check.name,
region: check.region,
status: result.status,
})
);
return result;
} catch (error) {
console.error(
JSON.stringify({
event: 'regional_check_error',
monitor: check.name,
region: check.region,
error: error instanceof Error ? error.message : String(error),
})
);
// Fall back to local check
console.warn(JSON.stringify({ event: 'regional_check_fallback', monitor: check.name }));
return executeLocalCheck(check);
}
} else {
// Run check locally (current behavior)
return executeLocalCheck(check);
}
}
async function executeLocalCheck(check: CheckRequest): Promise<CheckResult> {
console.warn(JSON.stringify({ event: 'local_check_start', monitor: check.name }));
switch (check.type) {
case 'http': {
return executeHttpCheck(check);
}
case 'tcp': {
return executeTcpCheck(check);
}
case 'dns': {
return executeDnsCheck(check);
}
}
}
async function sendWebhook(
alert: {
alertName: string;
monitorName: string;
alertType: string;
error: string;
timestamp: number;
},
config: Config
): Promise<boolean> {
try {
const payload = formatWebhookPayload({
alertName: alert.alertName,
monitorName: alert.monitorName,
alertType: alert.alertType,
error: alert.error,
timestamp: alert.timestamp,
config,
});
if (!payload.url) {
console.error(JSON.stringify({ event: 'webhook_not_found', alert: alert.alertName }));
return false;
}
const response = await fetch(payload.url, {
method: payload.method,
headers: payload.headers,
body: payload.body,
});
return response.ok;
} catch (error_) {
console.error(
JSON.stringify({
event: 'webhook_failed',
alert: alert.alertName,
error: error_ instanceof Error ? error_.message : String(error_),
})
);
return false;
}
}
export { RegionalChecker } from './regional/checker.js';