Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
# `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<ts, SlackDeliveryReport>`) 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<SlackResponse>`
- `updateMessage(channel, ts, text, blocks?): Promise<SlackResponse>`
- `deleteMessage(channel, ts): Promise<SlackResponse>`
- `listChannels(limit?): Promise<SlackChannel[]>`
- `getChannelInfo(channelId): Promise<SlackChannel>`
- `listUsers(limit?): Promise<SlackUser[]>`
- `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.