Monitor websites uptime using Cloudflare Workers
Something went wrong. Try again.
15 kB · 407 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408import { afterEach, describe, expect, it, vi } from 'vitest';import { pgFirst } from '@uptime/cloudflare/postgres';
import { openProviderConfig, sealProviderConfig } from './credentials.js';import { createTestContext, login, request, type TestContext } from './testing/test-utils.js';
const contexts: TestContext[] = [];const json = (response: Response): Promise<any> => response.json();
async function context(options: Parameters<typeof createTestContext>[0] = {}) { const ctx = await createTestContext(options); contexts.push(ctx); return ctx;}
afterEach(async () => { for (const ctx of contexts.splice(0)) await ctx.close(); vi.restoreAllMocks();});
const telegram = { name: 'Operations Telegram', provider: 'telegram', enabled: true, config: { botToken: '123456:abcdefghijklmnopqrstuvwxyz', chatId: '-100123456' },};
describe('credential encryption', () => { it('round-trips a provider config through AES-GCM', async () => { const secret = 'c'.repeat(32); const sealed = await sealProviderConfig(secret, telegram.config); expect(sealed).toContain('encrypted'); expect(sealed).not.toContain('abcdefghijklmnopqrstuvwxyz'); const { config, encrypted } = await openProviderConfig(secret, sealed); expect(encrypted).toBe(true); expect(config).toEqual(telegram.config); });
it('rejects a tampered envelope', async () => { const secret = 'c'.repeat(32); const sealed = await sealProviderConfig(secret, telegram.config); const tampered = sealed.replace(/"encrypted":"v1\.[A-Za-z0-9_-]{4}/, '"encrypted":"v1.AAAA'); await expect(openProviderConfig(secret, tampered)).rejects.toThrow(); });
it('rejects decryption with the wrong secret', async () => { const sealed = await sealProviderConfig('a'.repeat(32), telegram.config); await expect(openProviderConfig('b'.repeat(32), sealed)).rejects.toThrow(); });});
describe('notification services contract', () => { it('requires authentication', async () => { const { app } = await context(); const response = await request(app, '/api/notification-services'); expect(response.status).toBe(401); });
it('creates a service and stores credentials encrypted at rest', async () => { const ctx = await context(); const { cookie } = await login(ctx.app); const response = await request(ctx.app, '/api/notification-services', { method: 'POST', cookie, body: JSON.stringify(telegram), }); expect(response.status).toBe(201); const service = (await json(response)).service; expect(service.config).toEqual({ chatId: '-100123456' }); expect(JSON.stringify(service)).not.toContain('botToken'); expect(JSON.stringify(service)).not.toContain('abcdefghijklmnopqrstuvwxyz');
const stored = await pgFirst<{ config: string }>( ctx.db, `SELECT config FROM ${ctx.db.table('notification_services')}`, ); expect(stored?.config).not.toContain('abcdefghijklmnopqrstuvwxyz'); expect(stored?.config).toContain('encrypted'); });
it('lists every provider with secrets redacted', async () => { const ctx = await context(); const { cookie } = await login(ctx.app); const providers = [ telegram, { name: 'Resend', provider: 'resend', config: { apiKey: 're_secret', from: 'a@example.com', to: ['b@example.com'], subject: 'Alert', }, }, { name: 'Gotify', provider: 'gotify', config: { serverUrl: 'https://push.example.com', applicationToken: 'gotify-secret', priority: 8, }, }, { name: 'Discord', provider: 'discord', config: { webhookUrl: 'https://discord.com/api/webhooks/123/secrettoken' }, }, { name: 'Webhook', provider: 'webhook', config: { webhookUrl: 'https://hooks.example.com/secret', bearerToken: 'bearer-secret' }, }, { name: 'SMTP', provider: 'smtp', config: { host: 'smtp.example.com', port: 587, security: 'starttls', username: 'uptime', password: 'smtp-secret', from: 'a@example.com', to: ['b@example.com'], }, }, { name: 'HA', provider: 'home-assistant', config: { serverUrl: 'http://ha.local:8123', accessToken: 'ha-secret', service: 'notify' }, }, ]; for (const provider of providers) { const created = await request(ctx.app, '/api/notification-services', { method: 'POST', cookie, body: JSON.stringify(provider), }); expect(created.status).toBe(201); } const list = await request(ctx.app, '/api/notification-services', { cookie }); const body = await list.text(); for (const secret of [ 'abcdefghijklmnopqrstuvwxyz', 're_secret', 'gotify-secret', 'secrettoken', 'hooks.example.com', 'bearer-secret', 'smtp-secret', 'ha-secret', ]) { expect(body).not.toContain(secret); } });
it('retains a saved secret when an edit submits a blank secret', async () => { const ctx = await context(); const { cookie } = await login(ctx.app); const created = await request(ctx.app, '/api/notification-services', { method: 'POST', cookie, body: JSON.stringify(telegram), }); const id = (await json(created)).service.id; const updated = await request(ctx.app, `/api/notification-services/${id}`, { method: 'PATCH', cookie, body: JSON.stringify({ name: 'Primary Telegram', config: { botToken: '', chatId: '-100123456' }, }), }); expect(updated.status).toBe(200); expect((await json(updated)).service.name).toBe('Primary Telegram');
const stored = await pgFirst<{ config: string }>( ctx.db, `SELECT config FROM ${ctx.db.table('notification_services')} WHERE id = $1`, [id], ); const { config } = await openProviderConfig('c'.repeat(32), stored!.config); expect(config.botToken).toBe(telegram.config.botToken); });
it('stores Bluesky credentials encrypted, redacts them, and keeps a blank edited password', async () => { const ctx = await context(); const { cookie } = await login(ctx.app); const appPassword = 'bsky-app-password'; const created = await request(ctx.app, '/api/notification-services', { method: 'POST', cookie, body: JSON.stringify({ name: 'Bluesky', provider: 'bluesky', config: { handle: ' @Ops.Example.com ', appPassword }, }), }); expect(created.status).toBe(201); const service = (await json(created)).service; expect(service.config).toEqual({ handle: 'ops.example.com' }); expect(JSON.stringify(service)).not.toContain(appPassword); const id = service.id as string;
const stored = await pgFirst<{ config: string }>( ctx.db, `SELECT config FROM ${ctx.db.table('notification_services')} WHERE id = $1`, [id], ); expect(stored!.config).not.toContain(appPassword); expect(stored!.config).toContain('encrypted');
const updated = await request(ctx.app, `/api/notification-services/${id}`, { method: 'PATCH', cookie, body: JSON.stringify({ config: { handle: 'New.Example.com', appPassword: '' } }), }); expect(updated.status).toBe(200); expect((await json(updated)).service.config).toEqual({ handle: 'new.example.com' }); const saved = await pgFirst<{ config: string }>( ctx.db, `SELECT config FROM ${ctx.db.table('notification_services')} WHERE id = $1`, [id], ); const { config } = await openProviderConfig('c'.repeat(32), saved!.config); expect(config).toEqual({ handle: 'new.example.com', appPassword }); });
it('rejects unrelated keys for a provider', async () => { const ctx = await context(); const { cookie } = await login(ctx.app); const created = await request(ctx.app, '/api/notification-services', { method: 'POST', cookie, body: JSON.stringify(telegram), }); const id = (await json(created)).service.id; const response = await request(ctx.app, `/api/notification-services/${id}`, { method: 'PATCH', cookie, body: JSON.stringify({ config: { webhookUrl: 'https://x.example.com' } }), }); // strip()/strict provider keys are enforced during merge, not the update schema. expect(response.status).toBe(400); expect((await json(response)).error.code).toBe('validation_error'); });
it('rejects invalid provider config without echoing credentials', async () => { const ctx = await context(); const { cookie } = await login(ctx.app); const invalid = 'http://example.com/not-a-webhook'; const response = await request(ctx.app, '/api/notification-services', { method: 'POST', cookie, body: JSON.stringify({ name: 'Invalid Discord', provider: 'discord', config: { webhookUrl: invalid }, }), }); expect(response.status).toBe(400); const text = await response.text(); expect(JSON.parse(text).error.code).toBe('validation_error'); expect(text).not.toContain(invalid); });
it('deletes a service', async () => { const ctx = await context(); const { cookie } = await login(ctx.app); const created = await request(ctx.app, '/api/notification-services', { method: 'POST', cookie, body: JSON.stringify(telegram), }); const id = (await json(created)).service.id; const deleted = await request(ctx.app, `/api/notification-services/${id}`, { method: 'DELETE', cookie, }); expect(deleted.status).toBe(204); const missing = await request(ctx.app, `/api/notification-services/${id}`, { method: 'DELETE', cookie, }); expect(missing.status).toBe(404); });
it('sends a test through the configured provider', async () => { const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response(null, { status: 204 })); const ctx = await context({ notificationFetch: fetchMock }); const { cookie } = await login(ctx.app); const webhookUrl = 'https://discord.com/api/webhooks/123456/secret-token'; const created = await request(ctx.app, '/api/notification-services', { method: 'POST', cookie, body: JSON.stringify({ name: 'Discord', provider: 'discord', config: { webhookUrl } }), }); const id = (await json(created)).service.id; const response = await request(ctx.app, `/api/notification-services/${id}/test`, { method: 'POST', cookie, }); expect(response.status).toBe(200); expect(await json(response)).toEqual({ success: true }); expect(fetchMock).toHaveBeenCalledOnce(); expect(String(fetchMock.mock.calls[0]?.[0])).toBe(`${webhookUrl}?wait=true`); });
it('sends a Bluesky test by logging in and creating a post', async () => { const fetchMock = vi .fn<typeof fetch>() .mockResolvedValueOnce(Response.json({ did: 'did:plc:operations' })) .mockResolvedValueOnce( Response.json({ id: 'did:plc:operations', alsoKnownAs: ['at://ops.example.com'], service: [ { id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://eurosky.social', }, ], }), ) .mockResolvedValueOnce( Response.json({ did: 'did:plc:operations', accessJwt: 'access-token', refreshJwt: 'refresh', }), ) .mockResolvedValueOnce( Response.json({ uri: 'at://did:plc:operations/app.bsky.feed.post/123' }), ); const ctx = await context({ notificationFetch: fetchMock }); const { cookie } = await login(ctx.app); const created = await request(ctx.app, '/api/notification-services', { method: 'POST', cookie, body: JSON.stringify({ name: 'Bluesky', provider: 'bluesky', config: { handle: '@ops.example.com', appPassword: 'bsky-secret' }, }), }); const id = (await json(created)).service.id; const response = await request(ctx.app, `/api/notification-services/${id}/test`, { method: 'POST', cookie, }); expect(response.status).toBe(200); expect(await json(response)).toEqual({ success: true }); expect(fetchMock).toHaveBeenCalledTimes(4); expect(String(fetchMock.mock.calls[0]?.[0])).toBe( 'https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=ops.example.com', ); expect(String(fetchMock.mock.calls[1]?.[0])).toBe('https://plc.directory/did:plc:operations'); expect(String(fetchMock.mock.calls[2]?.[0])).toBe( 'https://eurosky.social/xrpc/com.atproto.server.createSession', ); expect(String(fetchMock.mock.calls[3]?.[0])).toBe( 'https://eurosky.social/xrpc/com.atproto.repo.createRecord', ); expect(JSON.parse(String(fetchMock.mock.calls[3]?.[1]?.body))).toMatchObject({ repo: 'did:plc:operations', collection: 'app.bsky.feed.post', record: { $type: 'app.bsky.feed.post' }, }); const history = await pgFirst<{ external_url: string | null }>( ctx.db, `SELECT external_url FROM ${ctx.db.table('notification_history')} WHERE notification_service_id = $1`, [id], ); expect(history?.external_url).toBe('https://bsky.app/profile/did:plc:operations/post/123'); });
it('documents SMTP as unsupported in the Workers runtime', async () => { const ctx = await context(); const { cookie } = await login(ctx.app); const created = await request(ctx.app, '/api/notification-services', { method: 'POST', cookie, body: JSON.stringify({ name: 'SMTP', provider: 'smtp', config: { host: 'smtp.example.com', port: 587, security: 'starttls', username: 'uptime', password: 'smtp-secret', from: 'a@example.com', to: ['b@example.com'], }, }), }); const id = (await json(created)).service.id; const response = await request(ctx.app, `/api/notification-services/${id}/test`, { method: 'POST', cookie, }); expect(response.status).toBe(501); expect((await json(response)).error.code).toBe('provider_unsupported'); });});