6.0 KiB
Add @suaveplan/discord-client
Package: @suaveplan/discord-client
Status: PROPOSED
Type: new-package
Why
Genesis needs a Discord transport for operations notifications, bot-driven workflows, and webhook-based delivery from server-side jobs. The upstream package at ../suaveplan/packages/universal/discord-client/ covers bot-token messaging plus webhook delivery, but it bakes in globalThis.fetch and Date.now, throws BaseError from a parallel error hierarchy, and ships no OpenTelemetry instruments. Migrating into genesis re-grounds it on SuaveplanError + defineCodes, makes fetch/clock injectable for deterministic testing, and wires OTel counters and histograms so every Discord request is observable from day one.
What changes
- ADDED:
@suaveplan/discord-clientpackage atpackages/messaging/discord-client/. - ADDED:
DiscordBotClient— bot-token client withsendMessage,editMessage,deleteMessage,getChannel,getGuild,createReaction,getStoredMessage. - ADDED:
DiscordWebhook— webhook-URL delivery withsend,sendText,sendEmbedreturningDiscordDeliveryReport. - ADDED:
DISCORD_CODESfrozen registry with 11 codes namespacedDISCORD.*. - ADDED:
DiscordClientError extends SuaveplanError<DiscordClientCode>. - ADDED:
executeDiscordRequest— shared HTTP funnel emittingdiscord_client.request.total(Counter) anddiscord_client.request.duration_ms(Histogram). - ADDED:
parseRetryAfterMs— header parser for Discord's three rate-limit headers. - ADDED: Zod boundary schemas (
DiscordBotConfigSchema,DiscordWebhookConfigSchema,DiscordMessageSchema). - ADDED:
FetchLikeinjectable contract on every public client.
Out of scope
- Gateway / WebSocket support — bot REST + webhooks only, matching upstream parity.
- File / attachment uploads — upstream does not implement them.
- Slash-command registration — separate concern, future change.
Source mapping
This change derives from ../suaveplan/enterprise/discord-client.md and the upstream implementation at ../suaveplan/packages/universal/discord-client/.
| Requirement | Source lines | Notes |
|---|---|---|
| Generic Discord API client for webhooks, bot messaging, embeds | enterprise/discord-client.md L10-L13 |
one-line purpose verbatim |
| Zero browser/Node/React deps — runs in Bun, Node, browser | enterprise/discord-client.md L37 |
enforced via FetchLike injectable |
| Zod schemas for all public types | enterprise/discord-client.md L38 |
wired at every constructor + every send/sendMessage |
Deterministic behaviour — injectable clock, no module-scope Date.now() |
enterprise/discord-client.md L40 |
Clock injectable |
| OpenTelemetry instrumentation for all public entrypoints | enterprise/discord-client.md L41 |
counter + histogram in utils/http.ts |
| Structured errors with stable codes | enterprise/discord-client.md L42 |
DISCORD_CODES + DiscordClientError |
AbortSignal everywhere async |
enterprise/discord-client.md L43 |
AbortController per request |
| No mocks; real services via deterministic injectables | enterprise/discord-client.md L53 |
FetchLike, Clock stubs in tests |
sideEffects: false |
enterprise/discord-client.md L55 |
declared in package.json |
workspace:* internal, Bun catalog external |
enterprise/discord-client.md L56 |
enforced by tests/dep-graph.test.ts |
TypeScript strict — no any, no ts-ignore |
enterprise/discord-client.md L59 |
inherited from tsconfig.base.json |
| Public API stability — Zod boundary schemas | enterprise/discord-client.md L60 |
three schemas at every public boundary |
Error hierarchy — extends @suaveplan/error |
enterprise/discord-client.md L61 |
DiscordClientError extends SuaveplanError |
| Bot REST surface: send/edit/delete/getChannel/createReaction | upstream src/core/discord-bot-client/discord-bot-client.ts L75-L130 |
reimplemented fresh |
Webhook surface: send, sendText, sendEmbed, threaded routing |
upstream src/core/discord-webhook/discord-webhook.ts L81-L105 |
reimplemented fresh |
| Discord rate-limit header parsing (3 header variants) | upstream src/core/discord-bot-client/discord-bot-client.ts L26-L48 |
extracted to utils/rate-limit.ts |
| 401/403/404/429/non-ok response → DISCORD code mapping | upstream src/core/discord-bot-client/discord-bot-client.ts L167-L215 |
concentrated in utils/http.ts |
DiscordDeliveryReport for webhook responses |
upstream src/types/discord-client.types.ts L86-L97 |
shape preserved |
Requirements with no source line
| Requirement | Justification |
|---|---|
| 100% Istanbul coverage threshold | Genesis-wide policy, not package-specific |
### DISCORD-NN — requirement heading format |
OpenSpec conformance contract (openspec-conformance.ts regex) |
**Verified by:** lines at draft time |
Genesis OpenSpec migration skill mandate |
Zod parse failure → DISCORD.INVALID_CONFIG vs DISCORD.CONFIG_MISSING split |
Required so the registry's severity: fatal vs severity: error mapping is callable |
| OTel histogram alongside the counter | Genesis observability convention (matches @suaveplan/webhook-client) |
Verification approach
src/errors/discord-client-error.test.ts— covers DISCORD-01 (error-code registry).src/utils/rate-limit.test.ts— covers DISCORD-02 (rate-limit header parser).src/utils/http.test.ts— covers DISCORD-03 (response→error mapping), DISCORD-04 (timeout + abort), DISCORD-05 (OTel instrumentation with recordingMeterProvider).src/core/bot-client/discord-bot-client.test.ts— covers DISCORD-06 (bot REST surface), DISCORD-07 (Zod config validation), DISCORD-08 (in-memory message cache).src/core/webhook/discord-webhook.test.ts— covers DISCORD-09 (webhook delivery + report), DISCORD-10 (webhook Zod boundary + URL host check).tests/dep-graph.test.ts— pins runtime dep set andsideEffects: false.
Migration impact
None — additive only. No consumer of @suaveplan/discord-client exists yet in genesis.