Monitor websites uptime using Cloudflare Workers
Something went wrong. Try again.
9.8 kB · 294 lines
TypeScript
at optimization
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295import { createHmac } from 'node:crypto';import { afterEach, describe, expect, it, vi } from 'vitest';
import { regionIds, type RegionId } from '@uptime/regions';
import worker from './index.js';
const env = { PROBE_REGION: 'us-east' as const, PROBE_SIGNING_SECRET: 'a'.repeat(32), PROBE_REQUEST_MAX_SKEW_SECONDS: '60', PROBE_MAX_REQUEST_BYTES: '65536', PROBE_VERSION: 'test',};
function workerEnv(regionId: RegionId) { return { ...env, PROBE_REGION: regionId };}const context = { waitUntil() {}, passThroughOnException() {}, props: {}, tracing: {},} as unknown as ExecutionContext;
afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks();});
function signedRequest(payload: Record<string, unknown>): Request { const body = JSON.stringify(payload); const canonical = `v1\n${String(payload.issuedAt)}\n${String(payload.requestId)}\n${body}`; const signature = createHmac('sha256', env.PROBE_SIGNING_SECRET) .update(canonical) .digest('base64url'); return new Request('https://probe.test', { method: 'POST', body, headers: { 'content-type': 'application/json', 'x-uptime-issued-at': String(payload.issuedAt), 'x-uptime-request-id': String(payload.requestId), 'x-uptime-signature': signature, 'x-uptime-signature-version': 'v1', }, });}
describe('probe worker request guard', () => { it('rejects non-POST requests before any target fetch', async () => { const response = await worker.fetch(new Request('https://probe.test'), env, context); expect(response.status).toBe(405); });
it.each(regionIds)( 'accepts the canonical %s identity and rejects a mismatched identity', async (regionId) => { const issuedAt = new Date().toISOString(); const requestId = crypto.randomUUID(); const payload = { checkRunId: crypto.randomUUID(), monitorId: crypto.randomUUID(), windowStartedAt: issuedAt, regionId, url: 'https://example.com/health', timeoutMs: 1_000, method: 'GET', maxRedirects: 5, maxBodyBytes: 65_536, requestId, issuedAt, }; vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('ok', { status: 200 })));
const accepted = await worker.fetch(signedRequest(payload), workerEnv(regionId), context); expect(accepted.status).toBe(200);
const otherRegion = regionIds.find((candidate) => candidate !== regionId)!; const rejected = await worker.fetch(signedRequest(payload), workerEnv(otherRegion), context); expect(rejected.status).toBe(403); await expect(rejected.json()).resolves.toEqual({ error: { code: 'region_or_identity_mismatch' }, }); }, );
it('treats a fast HTTP 521 response as unreachable', async () => { const issuedAt = new Date().toISOString(); vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue(new Response('Web server is down', { status: 521 })), );
const response = await worker.fetch( signedRequest({ checkRunId: 'ec1e26af-4a95-47a1-9400-3ea1caf03000', monitorId: '0ceba4d4-dde0-4e7e-99d7-062517cfa3cf', windowStartedAt: issuedAt, regionId: 'us-east', url: 'https://example.com/health', timeoutMs: 1_000, method: 'GET', maxRedirects: 5, maxBodyBytes: 65_536, requestId: 'b46f9f80-7ea6-43c7-a674-4b9a57ac6745', issuedAt, }), env, context, );
await expect(response.json()).resolves.toMatchObject({ status: 'http_failure', success: false, httpStatus: 521, errorDetail: 'HTTP 521', }); });
it('executes five same-region probes in one bounded batch', async () => { let active = 0; let maximumActive = 0; const fetchMock = vi.fn(async () => { active += 1; maximumActive = Math.max(maximumActive, active); await Promise.resolve(); active -= 1; return new Response('ok', { status: 200 }); }); vi.stubGlobal('fetch', fetchMock); const issuedAt = new Date().toISOString(); const requestId = crypto.randomUUID(); const items = Array.from({ length: 5 }, (_, index) => ({ checkRunId: `00000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}`, monitorId: `00000000-0000-4000-8001-${String(index + 1).padStart(12, '0')}`, windowStartedAt: issuedAt, url: `https://example.com/${index}`, timeoutMs: 1_000, method: 'GET', maxRedirects: 5, maxBodyBytes: 65_536, }));
const response = await worker.fetch( signedRequest({ requestId, issuedAt, regionId: 'us-east', items }), env, context, ); const payload = (await response.json()) as { requestId: string; regionId: string; results: { checkRunId: string; monitorId: string; response: { success: boolean } }[]; };
expect(response.status).toBe(200); expect(payload).toMatchObject({ requestId, regionId: 'us-east' }); expect(payload.results).toHaveLength(5); expect(payload.results.every((result) => result.response.success)).toBe(true); expect(fetchMock).toHaveBeenCalledTimes(5); expect(maximumActive).toBe(2); });
it('returns final-response CDN evidence for a signed synthetic request', async () => { const fetchMock = vi.fn().mockResolvedValue( new Response('ok', { status: 200, headers: { 'cf-ray': '8f1234567890abcd-LHR', 'set-cookie': 'must-not-be-retained=secret', }, }), ); vi.stubGlobal('fetch', fetchMock); const issuedAt = new Date().toISOString(); const requestId = 'b46f9f80-7ea6-43c7-a674-4b9a57ac6745'; const body = JSON.stringify({ checkRunId: 'ec1e26af-4a95-47a1-9400-3ea1caf03000', monitorId: '0ceba4d4-dde0-4e7e-99d7-062517cfa3cf', windowStartedAt: issuedAt, regionId: 'us-east', url: 'https://example.com/start', timeoutMs: 1_000, method: 'GET', maxRedirects: 5, maxBodyBytes: 65_536, requestId, issuedAt, }); const response = await worker.fetch( signedRequest(JSON.parse(body) as Record<string, unknown>), env, context, ); const observation = (await response.json()) as Record<string, unknown>; expect(response.status).toBe(200); expect(observation).toMatchObject({ success: true, finalUrl: 'https://example.com/start', endpointEvidence: { finalHostname: 'example.com', signals: [{ name: 'cf-ray', value: '8f1234567890abcd-LHR' }], primaryCdn: { provider: 'cloudflare', reportedEdge: 'LHR', inferredContinent: 'europe', confidence: 'provider_reported', }, }, }); expect(observation.dnsDiagnostic).toBeNull(); expect(fetchMock).toHaveBeenCalledTimes(1); });
it('collects DNS candidates for the final redirect hostname without changing HTTP timing', async () => { const baseNow = Date.now(); let now = baseNow; vi.spyOn(Date, 'now').mockImplementation(() => now); const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url.startsWith('https://cloudflare-dns.com/')) { now += 500; const type = new URL(url).searchParams.get('type'); const answer = type === 'A' ? [{ type: 1, TTL: 120, data: '1.1.1.1' }] : type === 'AAAA' ? [{ type: 28, TTL: 120, data: '2606:4700:4700::1111' }] : [{ type: 5, TTL: 120, data: 'edge.example.net.' }]; return new Response( JSON.stringify({ Status: 0, Question: [ { name: 'final.example.', type: type === 'A' ? 1 : type === 'AAAA' ? 28 : 5 }, ], Answer: answer, }), { headers: { 'content-type': 'application/dns-json' } }, ); } if (url === 'https://example.com/start') { now = baseNow + 10; return new Response(null, { status: 302, headers: { location: 'https://final.example/ok' }, }); } now = baseNow + 25; return new Response('ok', { status: 200 }); }); vi.stubGlobal('fetch', fetchMock); const issuedAt = new Date().toISOString(); const response = await worker.fetch( signedRequest({ checkRunId: 'ec1e26af-4a95-47a1-9400-3ea1caf03000', monitorId: '0ceba4d4-dde0-4e7e-99d7-062517cfa3cf', windowStartedAt: issuedAt, regionId: 'us-east', url: 'https://example.com/start', timeoutMs: 1_000, method: 'GET', maxRedirects: 5, maxBodyBytes: 65_536, requestId: 'b46f9f80-7ea6-43c7-a674-4b9a57ac6745', issuedAt, dnsDiagnostic: { diagnosticId: '6bb57ec3-fbf8-4c4f-9fc7-9572440abcc2', windowStartedAt: '2026-08-30T00:00:00.000Z', deadlineMs: 2_000, }, }), env, context, ); const observation = (await response.json()) as Record<string, any>; expect(observation).toMatchObject({ success: true, finalUrl: 'https://final.example/ok', responseMs: 25, totalMs: 25, dnsDiagnostic: { finalHostname: 'final.example', status: 'success', cnameCandidates: ['edge.example.net'], aCandidates: [{ address: '1.1.1.1', ttl: 120 }], }, }); expect(fetchMock).toHaveBeenCalledTimes(5); const dnsUrls = fetchMock.mock.calls.slice(2).map(([input]) => String(input)); expect(dnsUrls.every((url) => new URL(url).searchParams.get('name') === 'final.example')).toBe( true, ); });});