# `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 ```ts 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 ```ts 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` - `sendText(text: string, signal?: AbortSignal): Promise` Errors thrown: `SlackConfigMissingError`, `SlackTimeoutError`, `SlackAuthError`, `SlackWebhookFailedError`. ## Implementation Notes The message-id format is `webhook---` 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.