import * as WebhookDestination from './WebhookDestination.js' const envelope = { chainId: 4217, createdAt: '2026-01-01T00:00:00.000Z', data: { amount: '1000', token: `0x${'aa'.repeat(20)}` }, id: 'evt_test', subscriptionId: 'wh_test', type: 'token:transfer', } satisfies WebhookDestination.deliver.Options['envelope'] describe('url', () => { test('builds a url destination with a deliver method', () => { const destination = WebhookDestination.url('https://example.com/hook') expect(destination).toMatchObject({ type: 'url', url: 'https://example.com/hook' }) expect(typeof destination.deliver).toBe('function') // The method drops out of JSON — a destination serializes to just its data. expect(JSON.parse(JSON.stringify(destination))).toEqual({ type: 'url', url: 'https://example.com/hook', }) }) test('rejects a blocked host', () => { expect(() => WebhookDestination.url('https://169.254.169.254/latest/meta-data'), ).toThrowErrorMatchingInlineSnapshot( `[Webhooks.InvalidUrlError: Webhook URL rejected (blocked_host): https://169.254.169.254/latest/meta-data]`, ) }) }) describe('slack', () => { test('builds a slack destination', () => { expect(WebhookDestination.slack('https://hooks.slack.com/services/T/B/x')).toMatchObject({ type: 'slack', url: 'https://hooks.slack.com/services/T/B/x', }) }) test('rejects a non-Slack host', () => { expect(() => WebhookDestination.slack('https://evil.example.com/hook'), ).toThrowErrorMatchingInlineSnapshot( `[Webhooks.InvalidUrlError: Webhook URL rejected (slack_host): https://evil.example.com/hook]`, ) }) }) describe('betterStack', () => { test('builds a betterstack destination', () => { expect( WebhookDestination.betterStack({ token: 'src_token', url: 'https://s1.eu-nbg-2.betterstackdata.com', }), ).toMatchObject({ token: 'src_token', type: 'betterstack', url: 'https://s1.eu-nbg-2.betterstackdata.com', }) }) test('rejects a non-Better Stack host', () => { expect(() => WebhookDestination.betterStack({ token: 'src_token', url: 'https://evil.example.com' }), ).toThrowErrorMatchingInlineSnapshot( `[Webhooks.InvalidUrlError: Webhook URL rejected (betterstack_host): https://evil.example.com]`, ) }) }) describe('from', () => { test('normalizes a bare URL string into a url destination', () => { expect(WebhookDestination.from('https://example.com/hook')).toMatchObject({ type: 'url', url: 'https://example.com/hook', }) }) test('validates and returns a destination object', () => { expect( WebhookDestination.from({ type: 'slack', url: 'https://hooks.slack.com/services/T/B/x' }), ).toMatchObject({ type: 'slack', url: 'https://hooks.slack.com/services/T/B/x' }) }) test('rejects an invalid destination', () => { expect(() => WebhookDestination.from({ type: 'slack', url: 'https://evil.example.com' }), ).toThrowErrorMatchingInlineSnapshot( `[Webhooks.InvalidUrlError: Webhook URL rejected (slack_host): https://evil.example.com]`, ) }) }) describe('isRetryable', () => { test.each([ ['success', { ok: true, status: 204 }, false], ['redirect', { error: 'redirect rejected (302)', ok: false, status: 302 }, false], ['bad request', { error: 'non-2xx response (400)', ok: false, status: 400 }, false], ['request timeout', { error: 'non-2xx response (408)', ok: false, status: 408 }, true], ['rate limited', { error: 'non-2xx response (429)', ok: false, status: 429 }, true], ['server error', { error: 'non-2xx response (503)', ok: false, status: 503 }, true], ['opaque redirect', { error: 'redirect rejected (0)', ok: false }, false], ['timeout', { error: 'timed out after 10000ms', ok: false }, true], ['network error', { error: 'TypeError: fetch failed', ok: false }, true], ['unknown error', { ok: false }, true], ] satisfies readonly [string, WebhookDestination.Result, boolean][])( '%s', (_label, result, retryable) => { expect(WebhookDestination.isRetryable(result)).toBe(retryable) }, ) }) describe('url().deliver', () => { test('signs the body and POSTs a 2xx as success', async () => { let captured: | { body: string; headers: Headers; method?: string | undefined; url: string } | undefined const fetch = (async (input, init) => { captured = { body: typeof init?.body === 'string' ? init.body : '', headers: new Headers(init?.headers), method: init?.method, url: typeof input === 'string' ? input : '', } return new Response(null, { status: 200 }) }) satisfies typeof globalThis.fetch const secret = 'whsec_test' const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({ envelope, fetch, now: () => 0, secret, timestamp: 1_000, }) expect(result).toMatchInlineSnapshot(` { "durationMs": 0, "ok": true, "status": 200, } `) expect(captured?.method).toMatchInlineSnapshot(`"POST"`) expect(captured?.headers.get('tempo-event-id')).toMatchInlineSnapshot(`"evt_test"`) expect(captured?.headers.get('tempo-event-type')).toMatchInlineSnapshot(`"token:transfer"`) const signature = captured?.headers.get('tempo-signature') ?? '' expect( WebhookDestination.verify({ body: captured?.body ?? '', now: 1_000, secret, signature }), ).toMatchInlineSnapshot(`true`) }) test('classifies a non-2xx as failure with the status', async () => { const fetch = (async () => new Response(null, { status: 500 })) satisfies typeof globalThis.fetch const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({ envelope, fetch, now: () => 0, secret: 'whsec_test', }) expect(result).toMatchInlineSnapshot(` { "durationMs": 0, "error": "non-2xx response (500)", "ok": false, "status": 500, } `) }) test('rejects a 3xx redirect without following it (SSRF guard)', async () => { let redirectMode: RequestRedirect | undefined const fetch = (async (_input, init) => { redirectMode = init?.redirect return new Response(null, { headers: { location: 'https://169.254.169.254/latest' }, status: 302, }) }) satisfies typeof globalThis.fetch const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({ envelope, fetch, now: () => 0, secret: 'whsec_test', }) expect(redirectMode).toMatchInlineSnapshot(`"manual"`) expect(result).toMatchInlineSnapshot(` { "durationMs": 0, "error": "redirect rejected (302)", "ok": false, "status": 302, } `) }) test('fails a url delivery attempted without a secret', async () => { let called = false const fetch = (async () => { called = true return new Response(null, { status: 200 }) }) satisfies typeof globalThis.fetch const result = await WebhookDestination.url('https://hooks.example.com/x').deliver({ envelope, fetch, }) expect(called).toBe(false) expect(result).toMatchInlineSnapshot(` { "error": "url destination requires a secret", "ok": false, } `) }) }) describe('slack().deliver', () => { test('POSTs an unsigned Block Kit message to the incoming-webhook URL', async () => { let captured: { body: string; headers: Headers; url: string } | undefined const fetch = (async (input, init) => { captured = { body: typeof init?.body === 'string' ? init.body : '', headers: new Headers(init?.headers), url: typeof input === 'string' ? input : '', } return new Response(null, { status: 200 }) }) satisfies typeof globalThis.fetch const result = await WebhookDestination.slack( 'https://hooks.slack.com/services/T000/B000/xxxx', ).deliver({ envelope, fetch, now: () => 0 }) expect(result).toMatchInlineSnapshot(` { "durationMs": 0, "ok": true, "status": 200, } `) expect(captured?.url).toBe('https://hooks.slack.com/services/T000/B000/xxxx') expect(captured?.headers.get('tempo-signature')).toBeNull() const payload = JSON.parse(captured?.body ?? '{}') as { blocks: unknown[]; text: string } expect(Array.isArray(payload.blocks)).toBe(true) expect(payload.text).toMatchInlineSnapshot(`"Token transfer · 1000 · chain 4217"`) }) test('leads with the subscription context (title header + description section)', async () => { let captured: { body: string } | undefined const fetch = (async (_input, init) => { captured = { body: typeof init?.body === 'string' ? init.body : '' } return new Response(null, { status: 200 }) }) satisfies typeof globalThis.fetch await WebhookDestination.slack('https://hooks.slack.com/services/T/B/x').deliver({ envelope: { ...envelope, context: { description: 'Notify #ops on big settles.', metadata: { env: 'prod', team: 'payments' }, title: 'Prod USDC transfers', }, }, fetch, now: () => 0, }) const payload = JSON.parse(captured?.body ?? '{}') as { blocks: { fields?: { text: string }[]; text?: { text: string }; type: string }[] text: string } // Title drives the header + notification fallback. expect(payload.text).toBe('Prod USDC transfers') expect(payload.blocks[0]).toMatchObject({ text: { text: 'Prod USDC transfers' }, type: 'header', }) // Description gets its own section, before the field grid. expect(payload.blocks[1]).toMatchObject({ text: { text: 'Notify #ops on big settles.' }, type: 'section', }) // Metadata renders as its own field grid. const metadata = payload.blocks.find((block) => block.fields?.some((f) => f.text.startsWith('*env*')), ) expect(metadata?.fields).toEqual([ { text: '*env*\nprod', type: 'mrkdwn' }, { text: '*team*\npayments', type: 'mrkdwn' }, ]) // The raw event type stays in the context line so identity survives the title. const context = payload.blocks.at(-1) expect(context?.type).toBe('context') }) test('renders a token:transfer with a human amount and explorer links', async () => { const payload = await captureSlack({ chainId: 4217, createdAt: '2026-01-14T18:38:03.000Z', data: { address: '0x20c0000000000000000000008f5425160ebe5525', amount: '10000000', blockNumber: 1000002, recipient: '0x9e39034aae71fb89f66061a2602eb6efec271754', sender: '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38', timestamp: '2026-01-14T18:38:03.000Z', token: { decimals: 6, symbol: 'USDC' }, transactionHash: '0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3', }, id: 'evt_abc', subscriptionId: 'wh_abc', type: 'token:transfer', }) expect(header(payload)).toBe(':money_with_wings: Token transfer') // Human amount from token metadata, with truncated + linked sender/recipient. expect(section(payload, 1)).toContain('*10 USDC* from') expect(section(payload, 1)).toContain(' { const payload = await captureSlack({ chainId: 4217, createdAt: '2026-01-14T18:38:03.000Z', data: { blockNumber: 23456789, hash: '0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3', meta: { receipt: { status: 'reverted' } }, recipient: '0x9e39034aae71fb89f66061a2602eb6efec271754', sender: '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38', type: 'eip1559', value: '0', }, id: 'evt_tx', subscriptionId: 'wh_tx', type: 'transaction:included', }) expect(header(payload)).toBe(':x: Transaction reverted') expect(section(payload, 1)).toContain(':x: *reverted*') expect(payload.text).toBe('Transaction included · reverted · chain 4217') }) test('renders a decoded log with the event name in the header', async () => { const payload = await captureSlack({ chainId: 4217, createdAt: '2026-01-14T18:38:03.000Z', data: { address: '0x20c0000000000000000000008f5425160ebe5525', args: { from: '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38', value: '10000' }, blockNumber: 1000002, event: { name: 'Transfer', signature: 'event Transfer(address,address,uint256)' }, logIndex: 0, timestamp: '2026-01-14T18:38:03.000Z', transactionHash: '0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3', }, id: 'evt_log', subscriptionId: 'wh_log', type: 'log:emitted', }) expect(header(payload)).toBe(':label: Log: Transfer') expect(section(payload, 1)).toContain('*Transfer* on') expect(payload.text).toContain('Log: Transfer') }) test('renders a block:created header with the block number', async () => { const payload = await captureSlack({ chainId: 4217, createdAt: '2026-01-14T18:38:03.000Z', data: { gasLimit: 500000000, gasUsed: 4340281, hash: '0x3fe7d9e595f3d8215ec840c6ef55ac0bead58933e96fc2ead0f2aaf79715f45d', miner: '0x0000000000000000000000000000000000000000', number: 1000002, transactionCount: 3, timestamp: '2026-01-14T18:38:03.000Z', }, id: 'evt_blk', subscriptionId: 'wh_blk', type: 'block:created', }) expect(header(payload)).toBe(':link: Block created #1000002') expect(section(payload, 1)).toContain('*3* transactions') expect(payload.text).toBe('Block created #1000002 · 3 txns · chain 4217') }) test('renders a ping with a wired-up confirmation lead', async () => { const payload = await captureSlack({ chainId: 4217, createdAt: '2026-01-14T18:38:03.000Z', data: { ping: true }, id: 'evt_ping', subscriptionId: 'wh_ping', type: 'ping', }) expect(header(payload)).toBe(':bell: Webhook test ping') expect(section(payload, 1)).toContain('wired up correctly') expect(payload.text).toBe('Webhook test ping · chain 4217') }) test('classifies a non-2xx Slack response as failure', async () => { const fetch = (async () => new Response('invalid_token', { status: 403 })) satisfies typeof globalThis.fetch const result = await WebhookDestination.slack( 'https://hooks.slack.com/services/T000/B000/xxxx', ).deliver({ envelope, fetch, now: () => 0 }) expect(result).toMatchInlineSnapshot(` { "durationMs": 0, "error": "slack non-2xx response (403)", "ok": false, "status": 403, } `) }) }) /** A parsed Slack Block Kit payload with the loose block shapes we assert on. */ type SlackPayload = { blocks: { elements?: { text: string }[]; text?: { text: string }; type: string }[] text: string } /** Delivers an envelope through the `slack` transport and returns the parsed body. */ async function captureSlack( envelope: WebhookDestination.deliver.Options['envelope'], ): Promise { let body = '{}' const fetch = (async (_input, init) => { body = typeof init?.body === 'string' ? init.body : '{}' return new Response(null, { status: 200 }) }) satisfies typeof globalThis.fetch await WebhookDestination.slack('https://hooks.slack.com/services/T/B/x').deliver({ envelope, fetch, now: () => 0, }) return JSON.parse(body) as SlackPayload } /** The header block's text. */ function header(payload: SlackPayload): string | undefined { return payload.blocks.find((block) => block.type === 'header')?.text?.text } /** The mrkdwn text of the section block at `index` (header is block 0). */ function section(payload: SlackPayload, index: number): string { return payload.blocks[index]?.text?.text ?? '' } describe('betterStack().deliver', () => { test('POSTs a bearer-authed log event to the ingest URL', async () => { let captured: { body: string; headers: Headers; url: string } | undefined const fetch = (async (input, init) => { captured = { body: typeof init?.body === 'string' ? init.body : '', headers: new Headers(init?.headers), url: typeof input === 'string' ? input : '', } return new Response(null, { status: 202 }) }) satisfies typeof globalThis.fetch const result = await WebhookDestination.betterStack({ token: 'src_token', url: 'https://s1234567.eu-nbg-2.betterstackdata.com', }).deliver({ envelope, fetch, now: () => 0 }) expect(result).toMatchInlineSnapshot(` { "durationMs": 0, "ok": true, "status": 202, } `) expect(captured?.headers.get('authorization')).toBe('Bearer src_token') expect(captured?.headers.get('tempo-signature')).toBeNull() const payload = JSON.parse(captured?.body ?? '{}') as Record expect(payload['dt']).toBe('2026-01-01T00:00:00.000Z') expect(payload['event']).toBe('token:transfer') }) test('maps subscription context onto the log message + description', async () => { let captured: { body: string } | undefined const fetch = (async (_input, init) => { captured = { body: typeof init?.body === 'string' ? init.body : '' } return new Response(null, { status: 202 }) }) satisfies typeof globalThis.fetch await WebhookDestination.betterStack({ token: 'src_token', url: 'https://s1234567.eu-nbg-2.betterstackdata.com', }).deliver({ envelope: { ...envelope, context: { description: 'Notify #ops on big settles.', metadata: { env: 'prod', team: 'payments' }, title: 'Prod USDC transfers', }, }, fetch, now: () => 0, }) const payload = JSON.parse(captured?.body ?? '{}') as Record expect(payload['message']).toBe('Prod USDC transfers') expect(payload['description']).toBe('Notify #ops on big settles.') expect(payload['metadata']).toEqual({ env: 'prod', team: 'payments' }) }) test('classifies a non-2xx Better Stack response as failure', async () => { const fetch = (async () => new Response('Unauthorized', { status: 403 })) satisfies typeof globalThis.fetch const result = await WebhookDestination.betterStack({ token: 'src_token', url: 'https://s1234567.eu-nbg-2.betterstackdata.com', }).deliver({ envelope, fetch, now: () => 0 }) expect(result).toMatchInlineSnapshot(` { "durationMs": 0, "error": "betterstack non-2xx response (403)", "ok": false, "status": 403, } `) }) }) describe('from(...).deliver', () => { test('rebuilds a plain (persisted) destination and delivers via its own transport', async () => { let capturedUrl: string | undefined const fetch = (async (input) => { capturedUrl = typeof input === 'string' ? input : '' return new Response(null, { status: 200 }) }) satisfies typeof globalThis.fetch // A plain destination as read back from the store — no bound `deliver`. const destination: WebhookDestination.Destination = { type: 'slack', url: 'https://hooks.slack.com/services/T000/B000/xxxx', } const result = await WebhookDestination.from(destination).deliver({ envelope, fetch, now: () => 0, }) expect(result.ok).toBe(true) expect(capturedUrl).toBe('https://hooks.slack.com/services/T000/B000/xxxx') }) }) describe('assertDeliverableUrl', () => { test('accepts a plain https URL', () => { expect(() => WebhookDestination.assertDeliverableUrl('https://hooks.example.com/x'), ).not.toThrow() }) test.each([ ['http (non-https)', 'http://hooks.example.com', 'protocol'], ['ftp scheme', 'ftp://hooks.example.com', 'protocol'], ['embedded credentials', 'https://user:pass@hooks.example.com', 'credentials'], ['localhost', 'https://localhost/x', 'blocked_host'], ['sub.localhost', 'https://api.localhost/x', 'blocked_host'], ['loopback v4', 'https://127.0.0.1/x', 'blocked_host'], ['private 10/8', 'https://10.1.2.3/x', 'blocked_host'], ['private 172.16/12', 'https://172.16.0.1/x', 'blocked_host'], ['private 192.168/16', 'https://192.168.0.1/x', 'blocked_host'], ['link-local / metadata', 'https://169.254.169.254/x', 'blocked_host'], ['cgnat', 'https://100.64.0.1/x', 'blocked_host'], ['gcp metadata host', 'https://metadata.google.internal/x', 'blocked_host'], ['ipv6 loopback', 'https://[::1]/x', 'blocked_host'], ['ipv6 link-local', 'https://[fe80::1]/x', 'blocked_host'], ['ipv6 unique-local', 'https://[fd00::1]/x', 'blocked_host'], ['ipv4-mapped loopback', 'https://[::ffff:127.0.0.1]/x', 'blocked_host'], ['malformed', 'not a url', 'malformed'], ])('rejects %s', (_label, url, reason) => { let error: unknown try { WebhookDestination.assertDeliverableUrl(url) } catch (caught) { error = caught } expect(error).toBeInstanceOf(WebhookDestination.InvalidUrlError) expect((error as WebhookDestination.InvalidUrlError).reason).toBe(reason) }) test('accepts a public IPv4 literal', () => { expect(() => WebhookDestination.assertDeliverableUrl('https://93.184.216.34/x')).not.toThrow() }) }) describe('assertSlackUrl', () => { test('accepts a hooks.slack.com https URL', () => { expect(() => WebhookDestination.assertSlackUrl('https://hooks.slack.com/services/T000/B000/xxxx'), ).not.toThrow() }) test('rejects a non-Slack host', () => { expect(() => WebhookDestination.assertSlackUrl('https://evil.example.com/hook'), ).toThrowErrorMatchingInlineSnapshot( `[Webhooks.InvalidUrlError: Webhook URL rejected (slack_host): https://evil.example.com/hook]`, ) }) test('rejects a non-https URL', () => { expect(() => WebhookDestination.assertSlackUrl('http://hooks.slack.com/services/x'), ).toThrowErrorMatchingInlineSnapshot( `[Webhooks.InvalidUrlError: Webhook URL rejected (protocol): http://hooks.slack.com/services/x]`, ) }) }) describe('redactSlackUrl', () => { test('masks the secret path, keeping the origin', () => { expect( WebhookDestination.redactSlackUrl('https://hooks.slack.com/services/T000/B000/secrettoken'), ).toMatchInlineSnapshot(`"https://hooks.slack.com/…"`) }) }) describe('assertBetterstackUrl', () => { test('accepts a betterstackdata.com https URL', () => { expect(() => WebhookDestination.assertBetterstackUrl('https://s1234567.eu-nbg-2.betterstackdata.com'), ).not.toThrow() }) test('rejects a non-Better Stack host', () => { expect(() => WebhookDestination.assertBetterstackUrl('https://evil.example.com/ingest'), ).toThrowErrorMatchingInlineSnapshot( `[Webhooks.InvalidUrlError: Webhook URL rejected (betterstack_host): https://evil.example.com/ingest]`, ) }) test('rejects a non-https URL', () => { expect(() => WebhookDestination.assertBetterstackUrl('http://s1.betterstackdata.com'), ).toThrowErrorMatchingInlineSnapshot( `[Webhooks.InvalidUrlError: Webhook URL rejected (protocol): http://s1.betterstackdata.com]`, ) }) })