npm
Source
Description
@wolfstar/http-framework-utilities is the @wolfstar/http-framework counterpart of
@sapphire/discord.js-utilities. It provides:
- Type guards over raw channel and interaction payloads.
- Permission helpers based on the interaction’s
app_permissions. - A
MessageBuilderthat validates bodies against Discord’s limits. PaginatedMessage, with lazy and embed-field variants.MessagePrompter, withconfirmandnumberprompts.- Pluggable session stores for paginated messages and prompts.
@wolfstar/discord-utilities, so a single import covers
both packages.
@wolfstar/http-framework and discord-api-types are peer dependencies.
Setup
Import the/register entrypoint in your setup module, before you construct the client:
wolfstar-pm (paginated message) and wolfstar-mp (message prompter) interaction handlers on
every process, not only on the process that first calls PaginatedMessage#run or MessagePrompter#run.
Usage
Type guards and permissions
can* helpers read the application’s permissions from the interaction’s app_permissions. There is no channel
cache to fall back on. A helper that cannot decide from the payload returns false instead of throwing.
MessageBuilder
toJSON() validates the body against Discord’s limits and throws a RangeError naming the offending field.
MessageBuilder has no setFiles, because the framework’s reply and update do not upload files. Only followup
does.
PaginatedMessage
run sends the first page and stores a session. The built-in first, previous, next, last, stop, and
select controls use that session to browse pages. The package’s own interaction handlers resolve later clicks.
Use LazyPaginatedMessage to resolve function pages only when they are first displayed. Lazy pages exist only on the
process that started the message.
Custom actions
Custom actions with arun callback must use ids other than the built-in ones listed in
PaginatedMessageBuiltinActionIds. addAction throws a TypeError otherwise. You can pass an action with a built-in
id and no run to restyle a default button:
Concurrent clicks
Each process handles clicks on the same message one at a time, so two quicknext clicks both apply. A click that
waits for a previous one is acknowledged at once with deferUpdate, then applied by editing the original response.
A custom action run on such a click receives an interaction that is already acknowledged (interaction.replied is
true). Use followup in that case, not reply or update.
stop leaves a { stopped: true } tombstone in the store until the session would have expired. A click already in
progress re-reads the session before saving, so it cannot bring the stopped session back.
If saving a click fails, the message keeps its current page and the user gets an ephemeral notice. Set the notice
process-wide with setDefaultSaveFailedReply. It defaults to DefaultSaveFailedReply
('Something went wrong, please try again.').
Deferred replies and follow-ups
run always sends a fresh reply. For a deferred reply or a follow-up, call start(ownerId) instead. It creates the
session without sending anything and returns { sessionId, payload }, so you send payload yourself:
null as ownerId to let anyone use the components, even when ownerOnly is true (the default).
PaginatedFieldMessageEmbed
Build pages of embed fields from a list of items:
make() builds the pages from the current items, formatter, and items per page, and replaces any pages added before
it. Call it after you configure the setters.
MessagePrompter
The prompter uses the response to the button click to remove its buttons, because an HTTP interaction gets exactly one
response. Send any message about the user’s answer with
followup, not reply.
run resolves with the answer, or null when timeout (default 60_000 ms) elapses first. timeout must be at
most MaximumTokenLifetime, because a prompt always edits its own '@original' reply with the interaction token.
MessagePrompter requires a process-scoped session store. run throws a TypeError before replying if the
configured store’s scope is 'shared'. See What a shared store rejects.
Session stores
By default,PaginatedMessage sessions live in an in-memory MemorySessionStore with scope: 'process'. If your bot
runs several replicas behind a load balancer, a click may land on a process that did not create the session. Use
setSessionStore with a RedisSessionStore (scope: 'shared') to share sessions across processes:
RedisSessionStore only needs get, set(key, value, 'PX', ms), and del. An ioredis Redis or Cluster
instance satisfies it, and the package does not depend on ioredis itself.
setSessionStore sets the store for everything that does not pass its own store option. If you pass store to a
single PaginatedMessage, register that store with registerSessionStore on every replica. Otherwise, clicks that
reach another replica expire:
/register import matters on every process. A click can land on a process that
never called run, and only a process with registered handlers can answer it.
What a shared store rejects
A shared store only holds JSON-serializable, eager pagination state. Before replying,PaginatedMessage#run and
#start throw a TypeError naming the offending field for:
- Lazy page functions. Use eager pages with a shared store.
- Custom action
runcallbacks. Only the built-in action ids are supported with a shared store. - Values that do not survive a JSON round trip: functions, symbols,
bigints,NaN,Infinity, class instances other than plain objects and arrays, and circular references.
MessagePrompter rejects a shared store outright, because the answer goes to an in-process waiter. If your default
store is shared, you can still prompt by passing a process-scoped store. This works only if your load balancer routes
the click back to the process that called run:
PaginatedMessage with no
run callbacks over MessagePrompter.
Timeout cleanup
PaginatedMessage#run and MessagePrompter#run schedule a best-effort cleanup of the message’s controls when the
session times out (idle for PaginatedMessage, timeout for MessagePrompter). start(ownerId) flows have no
timeout cleanup.
timeoutBehavior: 'disable' | 'remove'(default'disable') sets what cleanup does.'disable'greys out every button and select menu.'remove'deletes the action rows.stopalways disables.- Cleanup edits the message through the interaction webhook while the token is valid.
MaximumTokenLifetime(14 minutes) bounds sessions whose only edit credential is the interaction token: ephemeral replies, or replies with no channel id. A longeridleortimeoutthrows aRangeErrorin those cases. - For a non-ephemeral
PaginatedMessage#runreply,idlecan exceedMaximumTokenLifetime. After the token expires, cleanup falls back to the bot’s REST client (container.rest). This needs the bot to access the channel. For a user-installed app or a guild the bot is not in, the edit fails, is logged, and the controls stay visible. MessagePrompter#runalways capstimeoutatMaximumTokenLifetime.- Interaction tokens are never persisted or logged. They live only in an in-process registry, independent of any session store.
Expired or unknown sessions
When a click’s session is missing or expired, the built-in handlersupdate the clicked message with every component
disabled. They then send an ephemeral followup with the expiry notice, which defaults to DefaultExpiredReply
('This interaction has expired.').
Set the notice once at startup with setDefaultExpiredReply, and read it with getDefaultExpiredReply. It applies to
both paginated messages and prompters:
PaginatedMessage also takes an expiredReply option (or setExpiredReply). It applies only while the session is
still readable but this process cannot handle the click, for example a custom action or lazy page that only exists on
another process. MessagePrompter has no expiredReply option.
Gateway
The optional@wolfstar/http-framework-utilities/gateway subpath adds gateway-only helpers built on
@wolfstar/plugin-gateway structures. The main entrypoint never imports it.
@wolfstar/plugin-gateway (^0.8.0) is an optional peer dependency and requires Node.js 24.17 or newer. Gateway bots
must still serve their HTTP interactions endpoint (GatewayClient.start({ listen })), because button and select clicks
arrive as interactions. They must also import @wolfstar/http-framework-utilities/register.
Type guards and permissions
isTextBasedChannel, isGuildBasedChannel, isThreadChannel, isVoiceBasedChannel, isNsfwChannel,
isMessageInstance, isGuildMember, and the per-type guards (isTextChannel, isDMChannel, isNewsChannel, and
others) take gateway structures instead of raw payloads.
The async can* helpers compute the bot’s permissions in a channel structure. They resolve false when they cannot
decide, for example with no channel, no bot user, no gateway client, or a failed fetch. In DMs they resolve true,
except canRemoveAllReactions and canJoinVoiceChannel, which resolve false.
canReadMessages, canSendMessages, canSendEmbeds, canSendAttachments, canReact,
canRemoveAllReactions, and canJoinVoiceChannel.
Collectors
awaitMessages(channel, { time, max, filter }) and awaitReactions(message, { time, max, filter }) resolve once
max (default 1) values pass filter, or when time (milliseconds, required) elapses. time and max must be
positive integers. Both reject without a GatewayClient.
Filters run concurrently, but results are committed in arrival order. When time elapses, every event whose filter
already passed is returned. Events still being filtered are dropped.
awaitReactions collects { reaction, user, userId }. user is null when the gateway plugin knows the user neither
from the payload nor from its cache, so filter on userId, which is always set.
GatewayPaginatedMessage
A PaginatedMessage whose run(target, author?) also accepts a gateway Message (replied to) or a text-based channel
(sent to). author is the user allowed to use the controls. It defaults to the message’s author for a Message, and
to anyone for a channel. It is ignored for an interaction.
idle
can exceed MaximumTokenLifetime.
GatewayMessagePrompter
A MessagePrompter with two more strategies, whose run(target, author?) accepts the same targets. author defaults
to the interaction’s user or the message’s author, and is required for a channel target.
The
message and reaction strategies throw before sending when the gateway client lacks their intents. reaction
reacts with reactions (default ['✅', '❌']) in order. Custom emojis use the name:id form. Unicode matching is
exact, including variation selectors, so configure the form Discord sends for the emoji.
MaximumTokenLifetime. On an HTTP interaction it
is, for every strategy. timeout must be a positive integer, and at most 2^31 − 1 ms for message and reaction.
Every strategy needs a process-scoped session store.
Limitations
flagsare not applied.PaginatedMessageandMessagePrompteronly readcontent,embeds, andallowed_mentionsfrom a page orMessageBuilder.flagssuch asMessageFlags.Ephemeralare dropped. For an ephemeral paginated message or prompt, passflagsin thereplyorupdatecall yourself.- A shared store is last-write-wins for simultaneous clicks on the same message across replicas. The re-read before
saving narrows the window in which a click can undo a
stop, but cannot close it. - Timeout cleanup is best effort. Process shutdown, serverless suspension, missing permissions, or API failures can leave expired controls visible.
MessagePrompteronly works with a process-scoped store, so its answer cannot move to another process.
Migrating from @sapphire/discord.js-utilities
- The
messageandreactionprompter strategies are not on the main entrypoint, because an HTTP interactions bot receives neither messages nor reactions. UseGatewayMessagePrompter. - Type guards take raw payloads (
APIChannel,APIInteractionDataResolvedChannel, or interaction structures) instead of discord.js class instances. can*permission helpers take the interaction and use itsapp_permissionsbitfield instead of a cached channel.isMessageInstanceandisGuildBasedChannelByGuildKeyare not on the main entrypoint. They existed only for discord.js structures.