@went.tf/discord-bot-framework 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -152,15 +152,34 @@ If other code (e.g. a locale-file type, or a helper building a command
152
152
  mention) needs the literal name/id union as a *type*, derive it from the
153
153
  registry you already built instead of hand-writing a parallel
154
154
  `const enum CommandName { ... }` — that enum is exactly the duplication the
155
- registry's `const` type inference exists to avoid:
155
+ registry's `const` type inference exists to avoid. **This only works if each
156
+ command's own `name` stays a literal type up to the point it's passed into
157
+ `createChatInputCommandRegistry`** — a real gotcha, not a hypothetical one:
158
+ a plain, unannotated `const pingCommand = { name: 'ping', handle: ... };`
159
+ widens `name` to `string` right there (standard TS object-literal-property
160
+ widening, the same reason `const x = { n: 1 }; x.n` is `number` not `1`),
161
+ *before* it ever reaches the registry call, so `RegistryName` would silently
162
+ resolve to `string` too. `satisfies BotChatInputCommand` does **not** fix
163
+ this either — it only checks compatibility, it doesn't request a literal.
164
+ Pin each command's own name as an explicit generic argument instead (the
165
+ same pattern this package's own registry tests use):
156
166
 
157
167
  ```ts
158
- import { RegistryName } from '@went.tf/discord-bot-framework/interactions';
168
+ import { NamedChatInputCommand, RegistryName } from '@went.tf/discord-bot-framework/interactions';
169
+
170
+ const pingCommand: NamedChatInputCommand<Ctx, 'ping'> = { name: 'ping', handle: (i) => i.reply('pong') };
171
+ const searchCommand: NamedChatInputCommand<Ctx, 'search'> = { name: 'search', handle: (i) => i.reply('...') };
159
172
 
160
173
  const chatInputCommandRegistry = createChatInputCommandRegistry([pingCommand, searchCommand]);
161
174
  type ChatInputCommandName = RegistryName<typeof chatInputCommandRegistry>; // 'ping' | 'search'
162
175
  ```
163
176
 
177
+ (Object literals passed directly inline into the array — not through an
178
+ intermediate `const` — infer literally on their own, since TS 5's `const`
179
+ type parameter modifier on `createChatInputCommandRegistry` requests literal
180
+ inference for its argument; the explicit-generic form above is what you need
181
+ once each command lives in its own file, which is the normal case.)
182
+
164
183
  A command's name should only ever be written down in two places: its
165
184
  `commands.json` entry and its own registry object's `name` field — nothing
166
185
  else should define it again, only reference the same string (or the derived
@@ -433,6 +452,101 @@ const manager = await createShardManager({
433
452
  });
434
453
  ```
435
454
 
455
+ **Graceful deploys with `gracefulRespawnSignal`:** a plain process restart
456
+ kills every shard at once, then respawns them sequentially — Discord's
457
+ identify rate limit forces roughly one shard every 5s, so a bot with 20+
458
+ shards can be fully down for minutes on every deploy. Passing
459
+ `gracefulRespawnSignal: 'SIGUSR2'` registers a handler that calls the
460
+ manager's `respawnAll()` instead, which kills/respawns shards **one at a
461
+ time** on the same long-lived manager process — at most one shard is briefly
462
+ offline (a few seconds) at any point, not the whole fleet:
463
+
464
+ ```ts
465
+ const manager = await createShardManager({
466
+ token, botScriptPath, logger,
467
+ beforeSpawn: () => startupCommandsUpdate(logger),
468
+ gracefulRespawnSignal: 'SIGUSR2',
469
+ });
470
+ ```
471
+
472
+ Your deploy script then sends that signal to the running manager process
473
+ (e.g. `kill -s USR2 "$(pm2 pid my-bot)"` — note dash's `kill` builtin
474
+ rejects bash's `-SIGUSR2` form, use `-s USR2`) instead of restarting it,
475
+ whenever only shard-side code changed. Since a forked shard process re-reads
476
+ its script fresh off disk, this picks up ordinary code changes fine — but
477
+ NOT changes to the top-level process that calls `createShardManager` itself
478
+ (this file, its `beforeSpawn`, or anything it holds in memory), since that
479
+ process is never restarted. Fall back to a real restart for those, and for
480
+ anything that needs `beforeSpawn`'s one-time setup (e.g. slash command
481
+ registration) to re-run.
482
+
483
+ ### `@went.tf/discord-bot-framework/webhook` (experimental)
484
+
485
+ > **Experimental, unvalidated against a real bot.** Unlike every other
486
+ > subpath in this package, `./webhook` hasn't yet been proven against a real
487
+ > migration — see the scope note at the end of this section and CLAUDE.md's
488
+ > `./webhook` design-decision entry. The API may change in a minor/patch
489
+ > release until that validation happens, despite semver-major otherwise being
490
+ > reserved for breaking changes in this package.
491
+
492
+ An alternate, independent transport alongside `./client`'s gateway-based
493
+ `createBotClient`/`createShardManager`, for bots that want to receive
494
+ interactions over an HTTP Interactions Endpoint instead of holding a
495
+ WebSocket open. As with `createBotClient`/`createShardManager`, this doesn't
496
+ unify with the gateway path behind one entry point — pick one transport per
497
+ bot and use its functions directly.
498
+
499
+ `verifyInteractionRequest` checks a request's `X-Signature-Ed25519`/
500
+ `X-Signature-Timestamp` headers against your application's public key, using
501
+ Node's native `crypto` module (no `tweetnacl`/`discord-interactions`
502
+ dependency needed). `handleWebhookInteractionRequest` wraps that plus
503
+ Discord's PING→PONG endpoint-validation handshake around your own handler,
504
+ taking/returning plain data so it can be wired into any HTTP framework (or
505
+ none):
506
+
507
+ ```ts
508
+ import { handleWebhookInteractionRequest } from '@went.tf/discord-bot-framework/webhook';
509
+ import { createServer } from 'node:http';
510
+
511
+ createServer(async (req, res) => {
512
+ const chunks: Buffer[] = [];
513
+ for await (const chunk of req) chunks.push(chunk);
514
+ const rawBody = Buffer.concat(chunks);
515
+
516
+ const { status, body } = await handleWebhookInteractionRequest(
517
+ {
518
+ signature: req.headers['x-signature-ed25519'] as string,
519
+ timestamp: req.headers['x-signature-timestamp'] as string,
520
+ rawBody,
521
+ },
522
+ {
523
+ publicKey: env.DISCORD_PUBLIC_KEY,
524
+ logger,
525
+ onInteraction: (interaction) => handleInteraction(interaction), // bot-side: build an APIInteractionResponse
526
+ },
527
+ );
528
+ res.writeHead(status, { 'Content-Type': 'application/json' }).end(JSON.stringify(body));
529
+ }).listen(3000);
530
+ ```
531
+
532
+ `createWebhookInteractionResponder` gives you the REST calls needed *after*
533
+ that initial response — editing a deferred reply, sending a follow-up, or
534
+ deleting the reply — built on `@discordjs/rest` (already a peer dependency):
535
+
536
+ ```ts
537
+ import { createWebhookInteractionResponder } from '@went.tf/discord-bot-framework/webhook';
538
+
539
+ const responder = createWebhookInteractionResponder({ rest, applicationId, interactionToken: interaction.token });
540
+ await responder.editReply({ content: 'Done!' });
541
+ ```
542
+
543
+ **This is not a drop-in replacement for discord.js's `Interaction#reply()`/
544
+ `editReply()`/`options.get*()` surface.** It's intentionally the thinner,
545
+ already-validated half of that problem (signature verification, the PING
546
+ handshake, and the plain REST calls) — see CLAUDE.md's `./webhook`
547
+ design-decision entry for why a discord.js-`Interaction`-shaped compatibility
548
+ adapter isn't built here yet.
549
+
436
550
  ### `@went.tf/discord-bot-framework/dev`
437
551
 
438
552
  Live-reloads compiled command/interaction handler *implementations* during