first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
# `webhook/slack-webhook` — Slack incoming-webhook client
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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.