Guards are composable validation functions that run before a spark's action. They can validate conditions, check permissions, enforce rate limits, and narrow TypeScript types so your action receives a more specific type than the raw input.
Every guard receives (input) and returns one of:
{ ok: true, value }-- validation passed;valueis the (possibly narrowed) input{ ok: false, reason }-- validation failed;reasonis a human-readable explanation
Guards are used with commands, components, gateway events, and scheduled event sparks.
Every built-in guard carries metadata (attached via a unique symbol at creation time). Metadata enables:
requires-- Guards that must run before this one. Automatically prepended at define-time forcommandandcomponentsparks.incompatibleWith-- Spark types this guard cannot be used with. Validated at define-time; throwsAppError('ERR_GUARD_INCOMPATIBLE')if violated.channelResolver-- Returns the target channel from the guard's narrowed input. Used by*PermissionInguards.
This means you no longer need to manually order common dependencies like inCachedGuild -- they are auto-resolved:
// Before: manual ordering required
guards: [g.inCachedGuild, g.hasPermission(PermissionFlagsBits.ManageMessages)]
// After: auto-resolved (inCachedGuild is prepended automatically)
guards: [g.hasPermission(PermissionFlagsBits.ManageMessages)]Note
Auto-resolution only applies to command and component sparks. Gateway event and scheduled event sparks skip dependency resolution because their inputs aren't always interactions.
Unicorn ships with a set of guards covering the most common validation needs. Import them as a namespace from @/guards/built-in for convenient autocomplete:
import * as g from '@/guards/built-in';
// Use as: g.inCachedGuild, g.hasPermission(...), g.botHasPermissionIn(...), etc.Ensures the interaction is in a cached guild. Narrows the interaction type to GuildInteraction, which guarantees guild, guildId, and member are present and typed. Note that channel may still be null if the channel is not cached β use inTextChannel to guarantee a non-null text channel.
This is the most commonly used guard and is automatically prepended by permission and channel guards via dependency resolution.
import { SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const serverInfo = defineCommand({
command: new SlashCommandBuilder()
.setName('server-info')
.setDescription('Show server information'),
guards: [g.inCachedGuild],
action: async (interaction) => {
// interaction.guild, interaction.member, interaction.guildId are all guaranteed
await interaction.reply(`Server: ${interaction.guild.name} (${interaction.guild.memberCount} members)`);
},
});Failure message: "This can only be used in a server."
Ensures the interaction has a non-null, text-based guild channel. Narrows the interaction type to TextChannelInteraction, which guarantees channel is present and sendable. Requires inCachedGuild (auto-resolved).
Use this guard when your action needs to call methods like interaction.channel.send().
import { SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const announce = defineCommand({
command: new SlashCommandBuilder()
.setName('announce')
.setDescription('Send an announcement in this channel'),
guards: [g.inTextChannel],
action: async (interaction) => {
// interaction.channel is guaranteed non-null and text-based
await interaction.channel.send('Important announcement!');
await interaction.reply({ content: 'Announcement sent!', ephemeral: true });
},
});Failure message: "This can only be used in a text channel."
Checks that the invoking user has the specified guild-level permission(s). Requires inCachedGuild (auto-resolved).
import { MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import { attempt } from '@/core/lib/attempt';
import * as g from '@/guards/built-in';
export const purge = defineCommand({
command: new SlashCommandBuilder()
.setName('purge')
.setDescription('Delete messages in bulk')
.addIntegerOption(opt => opt.setName('count').setDescription('Number of messages').setRequired(true)),
guards: [g.inTextChannel, g.hasPermission(PermissionFlagsBits.ManageMessages)],
action: async (interaction) => {
const count = interaction.options.getInteger('count', true);
const result = await attempt(() => interaction.channel.bulkDelete(count));
if (result.isErr()) {
await interaction.reply({ content: 'Failed to delete messages.', flags: MessageFlags.Ephemeral });
return;
}
await interaction.reply({ content: `Deleted ${count} messages.`, flags: MessageFlags.Ephemeral });
},
});You can check multiple permissions at once:
guards: [g.hasPermission(PermissionFlagsBits.ManageMessages | PermissionFlagsBits.ManageChannels)]An optional second argument overrides the default failure message:
g.hasPermission(PermissionFlagsBits.Administrator, 'This command is restricted to administrators.')Default failure message: "You need the following permission(s): ManageMessages" (lists the resolved permission names).
Checks that the bot has the specified permission(s) at the guild level (not channel-specific). Requires inCachedGuild (auto-resolved).
For channel-level bot permission checks, use botHasPermissionIn instead.
import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const modCommand = defineCommand({
command: new SlashCommandBuilder()
.setName('mod')
.setDescription('Moderation command'),
guards: [g.botHasPermission(PermissionFlagsBits.ManageRoles)],
action: async (interaction) => {
await interaction.reply('Moderation action performed.');
},
});Default failure message: "I need the following permission(s): ManageRoles"
Checks that the invoking user has the specified permission(s) in a specific channel. Requires inCachedGuild (auto-resolved).
When channelGuard is omitted, checks the interaction channel. When provided, reads the target channel from the guard's channelResolver metadata.
import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
// Check perms in the interaction channel
export const sendEmbed = defineCommand({
command: new SlashCommandBuilder()
.setName('send-embed')
.setDescription('Send a rich embed'),
guards: [g.hasPermissionIn(PermissionFlagsBits.EmbedLinks)],
action: async (interaction) => {
await interaction.reply({ embeds: [/* ... */] });
},
});
// Check perms in a specific channel (system channel)
export const announce = defineCommand({
command: new SlashCommandBuilder()
.setName('announce')
.setDescription('Post to system channel'),
guards: [g.hasPermissionIn(PermissionFlagsBits.SendMessages, g.hasSystemChannel)],
action: async (interaction) => {
await interaction.guild.systemChannel.send('Announcement!');
await interaction.reply({ content: 'Announcement posted!', ephemeral: true });
},
});Default failure message: "You need the following permission(s): EmbedLinks"
Checks that the bot has the specified permission(s) in a specific channel. Requires inCachedGuild (auto-resolved).
When channelGuard is omitted, checks the interaction channel. When provided, reads the target channel from the guard's channelResolver metadata.
import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
// Check bot perms in the interaction channel
export const embed = defineCommand({
command: new SlashCommandBuilder()
.setName('embed')
.setDescription('Send a rich embed'),
guards: [g.botHasPermissionIn(PermissionFlagsBits.EmbedLinks)],
action: async (interaction) => {
await interaction.reply({ embeds: [/* ... */] });
},
});
// Check bot perms in a specific channel
export const welcome = defineCommand({
command: new SlashCommandBuilder()
.setName('welcome')
.setDescription('Post to system channel'),
guards: [g.botHasPermissionIn(PermissionFlagsBits.SendMessages, g.hasSystemChannel)],
action: async (interaction) => {
await interaction.guild.systemChannel.send('Welcome message!');
await interaction.reply({ content: 'Posted!', ephemeral: true });
},
});Default failure message: "I need the following permission(s): EmbedLinks"
Checks that a channel with the given ID exists in the guild cache. Carries channelResolver metadata so *PermissionIn guards can read the target channel. Requires inCachedGuild (auto-resolved).
Accepts either a static ID string or a function that resolves the ID at execution time.
import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
// Static ID known at module scope
const logChannel = g.hasChannel('123456789012345678');
export const logCommand = defineCommand({
command: new SlashCommandBuilder()
.setName('log')
.setDescription('Post to the log channel'),
guards: [g.botHasPermissionIn(PermissionFlagsBits.SendMessages, logChannel)],
action: async (interaction) => {
// logChannel is guaranteed to exist, bot has SendMessages
},
});
// Dynamic ID resolved at execution time
const configChannel = g.hasChannel((input) => input.client.config.ids.channel.logs);Failure message: "Channel 123456789012345678 was not found in this server."
Ensures the interaction is in a channel of the specified type(s). Narrows the interaction's channel property to the matching type.
import { ChannelType, SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const threadOnly = defineCommand({
command: new SlashCommandBuilder()
.setName('thread-only')
.setDescription('Only works in threads'),
guards: [g.channelType(ChannelType.PublicThread, ChannelType.PrivateThread)],
action: async (interaction) => {
await interaction.reply('This is a thread!');
},
});Failure message: "This command can only be used in: PublicThread, PrivateThread" (lists the allowed channel type names).
Ensures the interaction is in a DM channel. Narrows the interaction type to DMInteraction, which guarantees channel is a DMChannel. Standalone β does not require inCachedGuild.
import { SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const dmOnly = defineCommand({
command: new SlashCommandBuilder()
.setName('dm-only')
.setDescription('Only works in DMs'),
guards: [g.isDMChannel],
action: async (interaction) => {
// interaction.channel is narrowed to DMChannel
await interaction.reply('This is a DM!');
},
});Failure message: "This can only be used in a direct message."
Restricts usage to a whitelist of user IDs. Useful for owner-only or admin-only commands.
import { SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const deploy = defineCommand({
command: new SlashCommandBuilder()
.setName('deploy')
.setDescription('Deploy slash commands'),
guards: [g.isUser(['123456789012345678', '987654321098765432'])],
action: async (interaction) => {
await interaction.reply({ content: 'Deploying commands...', ephemeral: true });
},
});Default failure message: "You do not have permission to use this command."
Filters out messages from bots. This guard operates on Message, not Interaction, and is intended for use with message-based gateway event sparks.
import { Events } from 'discord.js';
import { defineGatewayEvent } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const messageLogger = defineGatewayEvent({
event: Events.MessageCreate,
guards: [g.notBot],
action: (message, client) => {
client.logger.info({ content: message.content }, 'New message from a human');
},
});Failure message: "Bots cannot use this."
Ensures a message was sent in a guild (not a DM). Like notBot, this guard operates on Message and narrows it to Message<true>.
import { Events } from 'discord.js';
import { defineGatewayEvent } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const guildMessages = defineGatewayEvent({
event: Events.MessageCreate,
guards: [g.notBot, g.messageInGuild],
action: (message, client) => {
// message is narrowed to Message<true> β guild properties guaranteed
client.logger.info({ guild: message.guildId }, 'Guild message received');
},
});Failure message: "This can only be used in a server."
Limits how many times a user (or other key) can trigger an action within a time window. Uses an in-memory store with LRU eviction (bounded to 100k entries) to prevent unbounded memory growth.
| Option | Type | Required | Description |
|---|---|---|---|
limit |
number |
Yes | Maximum number of uses within the window |
window |
number |
Yes | Time window in milliseconds |
keyFn |
(input) => string |
No | Custom key function (default: user ID) |
message |
string |
No | Custom failure message |
import { SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
// 3 uses per 30 seconds per user
export const generate = defineCommand({
command: new SlashCommandBuilder()
.setName('generate')
.setDescription('Generate something expensive'),
guards: [g.rateLimit({ limit: 3, window: 30_000 })],
action: async (interaction) => {
await interaction.reply('Generating...');
},
});With a custom key function for per-guild rate limiting:
import { SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const announce = defineCommand({
command: new SlashCommandBuilder()
.setName('announce')
.setDescription('Make an announcement'),
guards: [
g.inCachedGuild,
g.rateLimit({
limit: 10,
window: 60_000,
keyFn: (interaction) => `${interaction.guildId}:${interaction.user.id}`,
}),
],
action: async (interaction) => {
await interaction.reply('Announcement sent!');
},
});Expired entries are cleaned up automatically via cleanupRateLimits(), which runs on a periodic interval.
Default failure message: "Rate limited. Try again in N seconds."
The special channel guards are constants that check if Discord's special guild channels exist. They check existence only -- use botHasPermissionIn to additionally verify bot permissions in the channel.
All special channel guards carry channelResolver metadata, so they can be passed to *PermissionIn guards to target the special channel.
Ensures the guild has a system channel configured. The system channel is used for welcome messages, boost notifications, and other system events.
import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const announce = defineCommand({
command: new SlashCommandBuilder()
.setName('announce')
.setDescription('Post an announcement to the system channel'),
guards: [g.botHasPermissionIn(PermissionFlagsBits.SendMessages, g.hasSystemChannel)],
action: async (interaction) => {
// interaction.guild.systemChannel is guaranteed to exist, bot can send
await interaction.guild.systemChannel.send('Important announcement!');
await interaction.reply({ content: 'Announcement posted!', ephemeral: true });
},
});For existence-only checks (no bot perm verification):
guards: [g.inCachedGuild, g.hasSystemChannel]Failure message: "This server does not have a system channel configured."
Ensures the guild has a public updates channel configured.
guards: [g.botHasPermissionIn(PermissionFlagsBits.SendMessages, g.hasPublicUpdatesChannel)]Failure message: "This server does not have a public updates channel configured."
Ensures the guild has a rules channel configured.
guards: [g.botHasPermissionIn(PermissionFlagsBits.SendMessages, g.hasRulesChannel)]Failure message: "This server does not have a rules channel configured."
Ensures the guild has a safety alerts channel configured.
guards: [g.botHasPermissionIn(PermissionFlagsBits.SendMessages, g.hasSafetyAlertsChannel)]Failure message: "This server does not have a safety alerts channel configured."
Guards execute sequentially. Each guard receives the output of the previous guard as its input. This means guards can progressively narrow the type:
guards: [g.inCachedGuild, g.hasPermission(PermissionFlagsBits.KickMembers)]
// ^ ^
// Narrows to Receives GuildInteraction (member guaranteed),
// GuildInteraction checks permissions on the memberIf any guard fails, the chain short-circuits and the remaining guards do not run.
For command and component sparks, resolveGuards() automatically prepends missing dependencies at define-time. This means you can write:
// Auto-resolves: [inCachedGuild, hasSystemChannel, botHasPermissionIn(SendMessages, hasSystemChannel)]
guards: [g.botHasPermissionIn(PermissionFlagsBits.SendMessages, g.hasSystemChannel)]The resolver:
- Checks each guard's
incompatibleWithmetadata against the spark type - Walks guards left-to-right and recursively prepends missing
requiresdependencies - Deduplicates by reference identity
- Corrects mis-ordered guards -- if a guard appears before its dependency, the dependency is moved ahead
Order still matters for guards without a declared dependency relationship. For example, if you have two unrelated guards A and B, they run in the order you specify. But for guards connected by requires, the resolver guarantees correct ordering regardless of how you list them.
Guards declare which spark types they're incompatible with. Using an incompatible guard throws at define-time:
// Throws ERR_GUARD_INCOMPATIBLE β inCachedGuild is incompatible with scheduled-event
defineScheduledEvent({
id: 'test',
schedule: '0 0 * * *',
guards: [g.inCachedGuild], // Error!
action: async () => {},
});With defineCommandGroup, guards compose at two levels:
- Top-level guards run for every subcommand. Use these for shared requirements like requiring a guild context.
- Per-subcommand guards run after the top-level guards pass. Use these for subcommand-specific validation like additional permission checks.
import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { defineCommandGroup } from '@/core/sparks';
import * as g from '@/guards/built-in';
export const channel = defineCommandGroup({
command: new SlashCommandBuilder()
.setName('channel')
.setDescription('Channel management')
.addSubcommand(sub => sub.setName('info').setDescription('View channel info'))
.addSubcommand(sub => sub.setName('lock').setDescription('Lock a channel'))
.addSubcommand(sub => sub.setName('nuke').setDescription('Delete and recreate a channel')),
// Runs for ALL subcommands
guards: [g.inCachedGuild],
subcommands: {
info: {
// No extra guards β anyone in the guild can view info
action: async (interaction) => {
await interaction.reply(`Channel: ${interaction.channel.name}`);
},
},
lock: {
// Only moderators can lock
guards: [g.hasPermission(PermissionFlagsBits.ManageChannels)],
action: async (interaction) => {
await interaction.reply('Channel locked.');
},
},
nuke: {
// Admins only, with rate limiting
guards: [
g.hasPermission(PermissionFlagsBits.Administrator),
g.rateLimit({ limit: 1, window: 300_000 }),
],
action: async (interaction) => {
await interaction.reply('Channel will be recreated.');
},
},
},
});The execution order for /channel nuke would be:
inCachedGuild (top-level)
-> hasPermission(Administrator) (per-subcommand)
-> rateLimit (per-subcommand)
-> action
If any guard in the chain fails, the action never runs.
Most of the time, automatic type narrowing handles this for you β when guards are present, the action callback's parameter is narrowed automatically. If you need to specify the narrowed type manually (e.g., when inference falls short or when no guards are used), the define* functions, SubcommandHandler, and defineGatewayEvent all accept an explicit TGuarded generic. For command-oriented functions it defaults to ChatInputCommandInteraction.
Pass the narrowed type explicitly:
import { type ChatInputCommandInteraction, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import { attempt } from '@/core/lib/attempt';
import * as g from '@/guards/built-in';
import type { GuildInteraction } from '@/guards/built-in';
export const kick = defineCommand<GuildInteraction<ChatInputCommandInteraction>>({
command: new SlashCommandBuilder()
.setName('kick')
.setDescription('Kick a member'),
guards: [g.inCachedGuild, g.hasPermission(PermissionFlagsBits.KickMembers)],
action: async (interaction) => {
const target = interaction.options.getUser('target', true);
const result = await attempt(() => interaction.guild.members.kick(target.id));
if (result.isErr()) {
await interaction.reply({ content: 'Failed to kick member.', flags: MessageFlags.Ephemeral });
return;
}
await interaction.reply(`Kicked ${target.username}`);
},
});Important: The generic is a type-level assertion -- TypeScript does not verify that your guards actually produce the declared narrowing. If you pass
GuildInteraction<ChatInputCommandInteraction>but omit theinCachedGuildguard, TypeScript won't complain, butinteraction.guildcould benullat runtime. Always keep your generic in sync with your guard chain.
Custom guards are built using createGuard, guardPass, and guardFail from @/core/guards. The optional second argument to createGuard attaches metadata.
A guard that doesn't narrow the type -- it just validates a condition:
import type { Interaction } from 'discord.js';
import { createGuard, type Guard, guardFail, guardPass } from '@/core/guards';
export const duringBusinessHours: Guard<Interaction, Interaction> = createGuard(
(interaction) => {
const hour = new Date().getUTCHours();
if (hour < 9 || hour >= 17) {
return guardFail('This command is only available during business hours (09:00-17:00 UTC).');
}
return guardPass(interaction);
},
{ name: 'duringBusinessHours' },
);A guard that narrows the input type, similar to inCachedGuild:
import type { ChatInputCommandInteraction, GuildMember, Interaction } from 'discord.js';
import { createGuard, type Guard, guardFail, guardPass } from '@/core/guards';
import * as g from '@/guards/built-in';
type InteractionWithBoostedMember = Interaction & {
member: GuildMember & { premiumSince: Date };
};
export const isServerBooster: Guard<
Interaction & { member: GuildMember },
InteractionWithBoostedMember
> = createGuard((interaction) => {
if (!interaction.member.premiumSince) {
return guardFail('This command is only available to server boosters.');
}
return guardPass(interaction as InteractionWithBoostedMember);
}, {
name: 'isServerBooster',
requires: [g.inCachedGuild],
});Use it after inCachedGuild to guarantee member is available (or let auto-resolution handle it):
export const boosterPerk = defineCommand({
command: new SlashCommandBuilder()
.setName('booster-perk')
.setDescription('A perk for server boosters'),
guards: [isServerBooster], // inCachedGuild auto-resolved
action: async (interaction) => {
await interaction.reply(`Boosting since ${interaction.member.premiumSince.toDateString()}!`);
},
});For guards that take configuration, return a guard from a factory function:
import type { Interaction } from 'discord.js';
import { createGuard, type Guard, guardFail, guardPass } from '@/core/guards';
export function requireOption(name: string): Guard<Interaction, Interaction> {
return createGuard((interaction) => {
if (!interaction.isChatInputCommand()) {
return guardFail('Not a command interaction.');
}
const value = interaction.options.get(name);
if (!value) {
return guardFail(`The "${name}" option is required.`);
}
return guardPass(interaction);
}, {
name: 'requireOption',
});
}When guards are provided to a define* function, the framework automatically narrows the action callback's parameter type using NarrowedBy<TBase, Guards>. This means guard output types flow through to the action without manual type assertions.
// Without guards: guild.systemChannel is `TextChannel | null` β requires `!`
const joinLogManual = defineGatewayEvent({
event: Events.GuildMemberAdd,
action: (member) => {
member.guild.systemChannel!.send('Welcome!'); // β non-null assertion
},
});
// With guards + auto-narrowing: guild.systemChannel is `TextChannel` β no assertion needed
export const joinLog = defineGatewayEvent({
event: Events.GuildMemberAdd,
guards: [g.hasSystemChannel],
action: (member) => {
member.guild.systemChannel.send('Welcome!'); // β type-safe
},
});This works via function overloads β when guards is an inline literal array (e.g. guards: [g.inCachedGuild]), TypeScript infers it as a const tuple and computes the intersection of the base type with each guard's output type via NarrowedBy<TBase, Guards>. When no guards are provided, the action receives the un-narrowed base type as before.
Note
const-inference only applies to inline literal arrays or arrays annotated with as const. A predeclared variable (e.g. const myGuards = [g.inCachedGuild]) is widened to its element type and won't trigger auto-narrowing. In that case, either use as const on the variable or provide TGuarded explicitly. You can always manually specify TGuarded as an explicit type parameter β the auto-narrowing overload is tried first, and the manual fallback applies when no guards are present or when an explicit type parameter is provided.
All spark types use processGuards() internally to run guards with centralized error handling. Developers never call this directly.
| Outcome | Level | Rationale |
|---|---|---|
| Intentional guard failure (user-facing) | info |
Dev can troubleshoot "it didn't work" reports without alert noise |
| Intentional guard failure (silent -- gateway/scheduled) | warn |
No user gets feedback, so higher visibility needed |
| Guard exception (bug in guard code) | error |
Programmer bug -- needs attention |
| Spark Type | Guard Failure | Guard Exception |
|---|---|---|
| Command | Ephemeral reply with reason + info log |
Ephemeral "something went wrong" + error log |
| Component | Ephemeral reply with reason + info log |
Ephemeral "something went wrong" + error log |
| Gateway Event | warn log only (no user to notify) |
error log only |
| Scheduled Event | warn log only (no user to notify) |
error log only |
When a guard fails, the spark's execute() method returns { ok: false, reason } without running the action. The interaction router then checks whether the interaction has already been replied to or deferred:
- If not replied to and not deferred, the router sends an ephemeral reply with the guard's
reasonstring as the message content. - If already replied to or deferred (e.g., by a guard that sends its own response), no additional reply is sent.
Guard failure reasons should be user-facing messages. Write them as clear, concise sentences that explain why the action was blocked.
| Type | Description |
|---|---|
Guard<TInput, TOutput> |
A guard function (input) => GuardResult<TOutput> |
GuardResult<T> |
{ ok: true, value: T } or { ok: false, reason: string } |
GuardOutput<G> |
Extracts the output type from a Guard type |
NarrowedBy<TBase, Guards> |
Intersects a base type with all guard output types β used by define* overloads for automatic type narrowing |
GuardMeta |
Metadata attached to a guard: name, requires?, incompatibleWith?, channelResolver? |
SparkType |
'command' | 'component' | 'gateway-event' | 'scheduled-event' |
GuildInteraction<T> |
Interaction with guild, guildId, and member guaranteed; channel may be null |
TextChannelInteraction<T> |
GuildInteraction with channel guaranteed non-null and text-based |
DMInteraction<T> |
Interaction with channel narrowed to DMChannel |
ChannelTypedInteraction<T, C> |
Interaction with channel narrowed to a specific ChannelType |
| Function | Description |
|---|---|
createGuard(fn, meta?) |
Wraps a guard function with proper type inference and optional metadata |
guardPass(value) |
Creates a successful GuardResult |
guardFail(reason) |
Creates a failed GuardResult |
runGuard(guard, input) |
Runs a single guard |
runGuards(guards, input) |
Runs guards sequentially, short-circuiting on failure |
getGuardMeta(guard) |
Reads metadata from a guard, or undefined if none |
resolveGuards(guards, sparkType) |
Validates compatibility + auto-resolves dependencies (define-time) |
processGuards(guards, input, logger, context, options?) |
Runs guards with centralized error handling and logging (execute-time) |
cleanupRateLimits() |
Clears expired rate limit entries from the in-memory store |
| Guard | Type | Description |
|---|---|---|
inCachedGuild |
constant | Narrows to guild interaction |
inTextChannel |
constant | Narrows to non-null text channel (requires inCachedGuild) |
isDMChannel |
constant | Narrows to DM channel |
hasPermission(perms, msg?) |
factory | Checks user guild-level permissions |
botHasPermission(perms, msg?) |
factory | Checks bot guild-level permissions |
hasPermissionIn(perms, channelGuard?, msg?) |
factory | Checks user channel-level permissions |
botHasPermissionIn(perms, channelGuard?, msg?) |
factory | Checks bot channel-level permissions |
hasChannel(idOrFn) |
factory | Checks channel exists in guild cache |
channelType(...types) |
factory | Narrows to channel type |
isUser(ids, msg?) |
factory | Whitelist by user ID |
notBot |
constant | Filters bot messages |
messageInGuild |
constant | Ensures message is in a guild |
rateLimit(opts) |
factory | Rate limits by key |
hasSystemChannel |
constant | Ensures system channel exists |
hasPublicUpdatesChannel |
constant | Ensures public updates channel exists |
hasRulesChannel |
constant | Ensures rules channel exists |
hasSafetyAlertsChannel |
constant | Ensures safety alerts channel exists |