6.5 KiB
Add @suaveplan/slack-client
Package: @suaveplan/slack-client
Status: PROPOSED
Type: new-package
Why
Genesis needs a typed, deterministic, OpenTelemetry-instrumented Slack client for transactional messaging (alerting, notifications, ops bots). The upstream @suaveplan/slack-client@1.0.1 at ../suaveplan/packages/universal/slack-client/ works but does not meet genesis enterprise-quality bar — it monkey-patches globalThis.fetch in tests (vi.stubGlobal), has no Zod boundary validation, no OTel meters, no injectable Clock/Rng/Logger, leaks as unknown casts, and registers codes through the legacy ErrorCodes.registerMany runtime registry instead of defineCodes. This change ships a fresh build at enterprise quality with full functional parity.
What changes
- ADDED:
@suaveplan/slack-client@0.1.0underpackages/messaging/slack-client/ - ADDED:
SlackApiClient— Slack Web API:sendMessage,updateMessage,deleteMessage,listChannels,getChannelInfo,listUsers,getStatus - ADDED:
SlackWebhook— Slack incoming-webhook client:send,sendText - ADDED:
SLACK_CODESregistry (defineCodes("SLACK", [...])) with 8 reasons:API_ERROR,AUTH_ERROR,RATE_LIMITED,CHANNEL_NOT_FOUND,WORKSPACE_NOT_FOUND,CONFIG_MISSING,WEBHOOK_FAILED,TIMEOUT - ADDED: 8 typed error subclasses extending
SuaveplanError<SlackErrorCode>(one per reason) - ADDED: Zod schemas at every public boundary:
SlackApiClientOptionsSchema,SlackWebhookOptionsSchema,SlackMessageSchema,SlackBlockSchema,SlackBlockElementSchema,SlackTextObjectSchema - ADDED: OTel meter instruments —
slack.requests,slack.errors,slack.latency_ms,slack.webhook_sends— emitted from every production hot path - ADDED: Injectable transport bag —
Clock,Rng,FetchLike,LoggerLike— all from@suaveplan/types - ADDED: Deterministic
FakeFetchtest double undersrc/fakes/for parity tests (novi.mockanywhere) - ADDED: Retry-After parser supporting delay-seconds, HTTP-date, and
x-ratelimit-resetepoch/ISO - ADDED: Slack-error string classifier (
invalid_auth/channel_not_found/team_not_foundfamilies)
Out of scope
None. The package ships at full functional parity with the upstream package plus the enterprise upgrades demanded by enterprise/slack-client.md §3.
Source mapping
This change derives from ../suaveplan/enterprise/slack-client.md and the working implementation at ../suaveplan/packages/universal/slack-client/src/.
| Requirement | Source lines | Notes |
|---|---|---|
| Generic Slack API client + webhook surface | enterprise/slack-client.md L10-L13 | verbatim |
| Zero browser/Node/React deps (universal tier) | enterprise/slack-client.md L37 | verbatim |
| Zod schemas at every boundary | enterprise/slack-client.md L38 | verbatim |
Deterministic — no module-scope Math.random / Date.now, injectable clock/RNG |
enterprise/slack-client.md L40 | verbatim |
OpenTelemetry instrumentation via @opentelemetry/api |
enterprise/slack-client.md L41 | verbatim |
Structured errors extending @suaveplan/error with stable codes |
enterprise/slack-client.md L42, L61 | verbatim |
| AbortSignal cancellation everywhere async | enterprise/slack-client.md L43 | verbatim |
| Idempotency + retry primitives wired | enterprise/slack-client.md L44 | applied to Retry-After / rate-limit; cooperative retry left to caller's @suaveplan/retry (no double-wrap) |
| i18n-ready (no hard-coded user-visible strings) | enterprise/slack-client.md L46 | every thrown error carries a stable code + structured context; messages are templated |
| Test coverage 100% statement/branch/function/line (istanbul) | enterprise/slack-client.md L50 + genesis CLAUDE.md "Quality" | genesis policy is 100%, stricter than the 95% in the source |
| File-size budget 250/300 soft/hard, 500 test | enterprise/slack-client.md L52 + CLAUDE.md | enforced by manual split |
| No mocks — injectable fakes only | enterprise/slack-client.md L53 + CLAUDE.md rule 2 | verbatim |
sideEffects: false |
enterprise/slack-client.md L55 | verbatim |
| Biome clean, zero suppressions | enterprise/slack-client.md L58 + CLAUDE.md rule 9 | verbatim |
TypeScript strict, no any / ts-ignore |
enterprise/slack-client.md L59 | verbatim |
| Error hierarchy with typed discriminants | enterprise/slack-client.md L61 | verbatim |
chat.postMessage, chat.update, chat.delete, conversations.list, conversations.info, users.list |
upstream packages/universal/slack-client/src/core/slack-api-client/slack-api-client.ts L52-L138 | parity |
| Webhook POST with text + blocks | upstream packages/universal/slack-client/src/core/slack-webhook/slack-webhook.ts L41-L105 | parity |
| HTTP 429 Retry-After / x-ratelimit-reset parsing | upstream slack-api-client.ts L259-L284 | parity |
Slack-error → code classifier (invalid_auth, channel_not_found, team_not_found) |
upstream slack-api-client.ts L286-L313 | parity |
Requirements with no source line
| Requirement | Justification |
|---|---|
| 100% coverage threshold (vs. 95% in source) | Genesis-wide policy — CLAUDE.md Quality section |
Test runner imports MUST go through @suaveplan/testing/runner |
Genesis rule 11 — not applicable to upstream which predates the rule |
defineCodes("SLACK", […]) registry style |
Genesis-wide error-codes convention — upstream used the legacy ErrorCodes.registerMany API |
FakeFetch test double + setup polyfills wiring real MeterProvider |
Genesis pre-archive checklist row 5 (OTel asserted with real MeterProvider, not vi.mock) |
Verification approach
src/client/slack-api-client.test.ts— pins everySlackApiClientmethod scenario withFakeFetchsrc/webhook/slack-webhook.test.ts— pins everySlackWebhookscenario withFakeFetchsrc/errors/errors.test.ts— pins error-code identity and message keys for all 8 codessrc/schemas/schemas.test.ts— Zod boundary parse + safeParse assertions for every public schemasrc/client/retry-after.test.ts— pins every Retry-After header form (delay-seconds, HTTP-date, x-ratelimit-reset epoch + ISO, missing)src/client/classify.test.ts— pins HTTP-status and slack-error-string classifierssrc/otel/metrics.test.ts— asserts the four OTel instruments emit on every production path, via real in-memoryMeterProvidersrc/fakes/fake-fetch.test.ts— pins the deterministic test double itselfsrc/index.test.ts— pins the public-surface barrel
Migration impact
None — additive. No existing consumer of @suaveplan/slack-client exists in genesis.