Files
skills/docs/communication/messaging/slack-client/src/webhook/slack-webhook.md
2026-05-29 08:20:56 +01:00

3.3 KiB

webhook/slack-webhook — Slack incoming-webhook client

(no submodules)

Purpose

SlackWebhook is the simpler half of the package — a typed wrapper around Slack's incoming-webhook URLs. Where SlackApiClient requires an OAuth token and supports the full Web API, the webhook client takes only the URL and POSTs { text, blocks? } payloads. It exists because many ops surfaces (CI bots, alerting, build notifications) only need fire-and-forget delivery and the webhook URL is the lower-friction credential — no token rotation, no scoped permissions. The client returns a SlackDeliveryReport on success and surfaces typed errors on every failure path so retry middleware can pattern-match on .code.

Features

  • Zod boundary validation in the constructor — invalid webhookUrl rejects with SlackConfigMissingError.
  • Three failure paths — network/timeout (SlackTimeoutError), HTTP 401/403 (SlackAuthError), other 4xx/5xx (SlackWebhookFailedError).
  • Injectable Clock for deterministic message-id timestamps.
  • Optional Rng injection — when supplied, the message-id suffix uses rng.nextUint32().toString(36) for uniqueness across rapidly-issued sends; otherwise falls back to the monotonic counter.
  • slack.webhook_sends and slack.errors OTel instruments emitted on every path.
  • Caller-supplied AbortSignal AND internal timeoutMs (default 10 000 ms) both honoured.

Usage: Basic

import { SlackWebhook } from "@suaveplan/slack-client";

const webhook = new SlackWebhook({
    webhookUrl: "https://hooks.slack.com/services/T0/B0/X",
});

const report = await webhook.sendText("Deploy complete.");

Usage: Advanced

import { SlackWebhook } from "@suaveplan/slack-client";
import { FakeFetch } from "@suaveplan/slack-client/fakes/fake-fetch";

const fake = new FakeFetch();
fake.push({ status: 200 });

let counter = 0;
const webhook = new SlackWebhook({
    webhookUrl: "https://hooks.slack.com/services/T/B/X",
    fetch: fake.fetch,
    clock: () => 1_700_000_000_000 + counter++,
    rng: { nextUint32: () => 42, nextFloat: () => 0.42 },
    logger: { debug() {}, info() {}, warn() {}, error() {} },
});

const report = await webhook.send({
    text: "Block kit",
    blocks: [{ type: "section", text: { type: "mrkdwn", text: "*Hi*" } }],
});

API Reference

  • new SlackWebhook(options: SlackWebhookOptions)
  • send(message: { text: string; blocks?: SlackBlock[] }, signal?: AbortSignal): Promise<SlackDeliveryReport>
  • sendText(text: string, signal?: AbortSignal): Promise<SlackDeliveryReport>

Errors thrown: SlackConfigMissingError, SlackTimeoutError, SlackAuthError, SlackWebhookFailedError.

Implementation Notes

The message-id format is webhook-<ms>-<counter>-<rngOrCounter> so even rapid bursts on the same clock tick produce distinct ids. The counter is monotone within a single instance; injecting an Rng adds cross-instance uniqueness without coupling to crypto.randomUUID (which is universally available but adds an unwanted dependency on the runtime's CSPRNG availability in test environments). The constructor explicitly binds globalThis.fetch to globalThis when used as default — without the bind, fetch.call(undefined, ...) triggers an "Illegal invocation" TypeError in some browser builds.