@went.tf/discord-bot-framework 2.5.0 → 2.7.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
@@ -480,14 +480,16 @@ process is never restarted. Fall back to a real restart for those, and for
480
480
  anything that needs `beforeSpawn`'s one-time setup (e.g. slash command
481
481
  registration) to re-run.
482
482
 
483
- ### `@went.tf/discord-bot-framework/webhook` (experimental)
483
+ ### `@went.tf/discord-bot-framework/webhook`
484
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.
485
+ > **Validated against a real bot.** HammerTimeBot ran this against a real
486
+ > registered Discord Interactions Endpoint URL (portal PING validation, then
487
+ > a live `/unix` slash command end-to-end) on its `migrate-discord-bot-framework`
488
+ > branch — signature verification, PING/PONG, the discord.js-`Interaction`
489
+ > bridge, and the REST-callback-based ack path all confirmed working with
490
+ > zero modifications needed to its 16 command files + component handler. See
491
+ > CLAUDE.md's `./webhook` design-decision entry for the one open item that
492
+ > validation surfaced (a Discord portal-side double-PING quirk, not blocking).
491
493
 
492
494
  An alternate, independent transport alongside `./client`'s gateway-based
493
495
  `createBotClient`/`createShardManager`, for bots that want to receive
@@ -518,6 +520,7 @@ createServer(async (req, res) => {
518
520
  signature: req.headers['x-signature-ed25519'] as string,
519
521
  timestamp: req.headers['x-signature-timestamp'] as string,
520
522
  rawBody,
523
+ headers: req.headers as Record<string, string | undefined>, // optional - enriches the signature-rejection debug log
521
524
  },
522
525
  {
523
526
  publicKey: env.DISCORD_PUBLIC_KEY,
@@ -529,6 +532,17 @@ createServer(async (req, res) => {
529
532
  }).listen(3000);
530
533
  ```
531
534
 
535
+ On a signature-verification failure, `handleWebhookInteractionRequest` logs a
536
+ `debug`-level diagnostic alongside the existing `warn` - source IP (checking
537
+ `cf-connecting-ip`/`x-real-ip`/`x-forwarded-for` in turn, for bots sitting
538
+ behind a reverse proxy), user-agent, whether the signature/timestamp headers
539
+ were present at all vs. present-but-wrong, and signature/body length. A
540
+ public webhook endpoint draws routine internet-scanner noise as well as
541
+ genuine misconfiguration; this is what tells the two apart after the fact,
542
+ without every consuming bot re-implementing the same wrapper. Pass `headers`
543
+ to populate it - omit it and the IP/user-agent fields just come back
544
+ `undefined`.
545
+
532
546
  `createWebhookInteractionResponder` gives you the REST calls needed *after*
533
547
  that initial response — editing a deferred reply, sending a follow-up, or
534
548
  deleting the reply — built on `@discordjs/rest` (already a peer dependency):
@@ -540,12 +554,54 @@ const responder = createWebhookInteractionResponder({ rest, applicationId, inter
540
554
  await responder.editReply({ content: 'Done!' });
541
555
  ```
542
556
 
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.
557
+ **Running existing gateway-shaped command handlers unmodified:**
558
+ `createWebhookOnlyClient` builds a real discord.js `Client` that's fully
559
+ authenticated for REST but never opens a gateway connection (`Client#login()`
560
+ always does, with no way to opt out, so this sets `client.rest`'s token
561
+ directly instead - both fully public API). `interactionFromWebhookPayload`
562
+ then reconstructs a genuine discord.js `Interaction` instance from the raw
563
+ payload, the same way discord.js's own gateway path does internally - so
564
+ `.reply()`/`.deferReply()`/`.editReply()`/`.options.getString()` etc. all work
565
+ exactly as they do today, and your existing `BotChatInputCommand`/
566
+ `dispatchChatInputCommand`/`createInteractionRouter` code needs zero changes:
567
+
568
+ ```ts
569
+ import { createWebhookOnlyClient, handleWebhookInteractionRequest, interactionFromWebhookPayload } from '@went.tf/discord-bot-framework/webhook';
570
+ import { dispatchChatInputCommand } from '@went.tf/discord-bot-framework/interactions';
571
+
572
+ const client = createWebhookOnlyClient({ token: env.DISCORD_BOT_TOKEN });
573
+
574
+ // inside your HTTP handler, in place of the plain onInteraction above:
575
+ onInteraction: async (data) => {
576
+ const interaction = interactionFromWebhookPayload(client, data);
577
+ if (interaction.isChatInputCommand()) {
578
+ await dispatchChatInputCommand(interaction, context, { commands: registry.byName, onError });
579
+ return; // .reply()/.deferReply() already sent the real response via REST
580
+ }
581
+ // ...similarly for dispatchComponent/dispatchModal/dispatchAutocomplete/dispatchContextMenu
582
+ },
583
+ ```
584
+
585
+ Two real gaps versus a gateway-delivered interaction, both from there being no
586
+ populated gateway cache: `.guild` is always `null`, and `.channel` is `null`
587
+ unless you pre-cache the interaction's inline partial channel data yourself.
588
+ `.member` degrades gracefully instead (falls back to the raw
589
+ `APIInteractionGuildMember` object), so plain property reads keep working.
590
+ See `interactionFromWebhookPayload`'s doc comment for the full detail -
591
+ runtime-verified in `interaction-from-webhook-payload.test.ts` (including the
592
+ two discord.js interaction classes with `private` constructors,
593
+ `ButtonInteraction`/`ModalSubmitInteraction`, which still construct correctly
594
+ through this bridge).
595
+
596
+ **The REST-callback-based ack path is confirmed working against live Discord
597
+ traffic**, not just source-reading: when a handler's `.reply()`/`.deferReply()`
598
+ call already sent the real response via REST mid-handler,
599
+ `handleWebhookInteractionRequest` sends a bare `{}` with a 200 as the literal
600
+ HTTP response to Discord's original webhook POST if `onInteraction` returns
601
+ nothing - HammerTimeBot confirmed this is accepted end-to-end via a real
602
+ `/unix` slash command run through a registered Interactions Endpoint URL. See
603
+ CLAUDE.md's `./webhook` design-decision entry for that validation's one open
604
+ item (a portal-side double-PING quirk, not blocking).
549
605
 
550
606
  ### `@went.tf/discord-bot-framework/dev`
551
607