# `client/slack-api-client` — Slack Web API client ## Purpose `SlackApiClient` is the typed transport for the Slack Web API. It exposes the six methods downstream packages rely on — `sendMessage`, `updateMessage`, `deleteMessage`, `listChannels`, `getChannelInfo`, `listUsers` — plus `getStatus` for retrieving the delivery report stored after a successful `sendMessage`. The client accepts an injectable transport bag (`fetch`, `clock`, `logger`) so production code can pass `globalThis.fetch` while tests inject a `FakeFetch`. Every public method is bounded by an internal timeout enforced via `AbortController`, supports a caller-supplied `AbortSignal`, classifies non-2xx responses through the shared HTTP / Slack-error matchers, and emits the four OTel instruments defined in `otel/metrics`. ## Features - Zod boundary validation in the constructor — empty `token` rejects with `SlackConfigMissingError` whose `cause` is the originating `ZodError`. - Eight typed error subclasses surfaced through `classify` + `#errorForCode`. - `Retry-After` and `x-ratelimit-reset` parsing on HTTP 429 — millisecond delta exposed via `error.context.retryAfterMs`. - snake_case → camelCase mapping for channel and user payloads with `undefined`-elision so the public shape stays compatible with `exactOptionalPropertyTypes`. - Internal `timeoutMs` (default 10 000 ms) AND caller `AbortSignal` both routed through one `AbortController`. - `slack.requests` / `slack.errors` / `slack.latency_ms` emitted from every code path. - Delivery-report cache (`Map`) for `getStatus(ts)` lookups. ## Usage: Basic ```ts import { SlackApiClient } from "@suaveplan/slack-client"; const client = new SlackApiClient({ token: process.env["SLACK_TOKEN"]! }); const res = await client.sendMessage({ channel: "C0123456789", text: "Build is green :white_check_mark:", }); ``` ## Usage: Advanced ```ts import { SlackApiClient } from "@suaveplan/slack-client"; import { FakeFetch } from "@suaveplan/slack-client/fakes/fake-fetch"; const fake = new FakeFetch(); fake.push({ json: { ok: true, channel: "C1", ts: "1.1" }, }); let now = 1_700_000_000_000; const client = new SlackApiClient({ token: "xoxb-test", fetch: fake.fetch, clock: () => now++, logger: { debug() {}, info() {}, warn() {}, error() {} }, }); const res = await client.sendMessage({ channel: "C1", text: "hi" }); const report = client.getStatus(res.ts!); // report.status === "delivered" ``` ## API Reference - `new SlackApiClient(options: SlackApiClientOptions)` - `sendMessage(message: SlackMessage, signal?: AbortSignal): Promise` - `updateMessage(channel, ts, text, blocks?): Promise` - `deleteMessage(channel, ts): Promise` - `listChannels(limit?): Promise` - `getChannelInfo(channelId): Promise` - `listUsers(limit?): Promise` - `getStatus(ts: string): SlackDeliveryReport | undefined` Errors thrown: `SlackConfigMissingError`, `SlackChannelNotFoundError`, `SlackAuthError`, `SlackRateLimitedError`, `SlackTimeoutError`, `SlackWorkspaceNotFoundError`, `SlackApiError`. ## Implementation Notes The client uses private `#`-prefixed class fields (not TS `private`) so consumer tooling cannot accidentally reach into the instance through bracket access — the contract is enforced at the runtime level by the engine. The request method assembles one `AbortController` that listens to both the caller signal AND the internal timer, then aborts on whichever fires first; the listener is removed in `finish()` to avoid leaking attached handlers across requests. The `#errorForCode` switch is exhaustive over `SlackErrorCode` and defaults to `SlackApiError` for forward-compatibility when the registry expands.