first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user