npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yaebal/guards

v0.1.0

Published

yaebal guards — reusable bot.guard() predicates: isAdmin, isPrivate, isGroup, hasMembership, hasPermission.

Readme

@yaebal/guards

reusable bot.guard() predicates so every project stops hand-rolling "is this user allowed" checks: isPrivate/isGroup reuse @yaebal/filters' chat-type filters, isAdmin/ hasMembership/hasPermission/hasAnyPermission/hasAllPermissions do a live getChatMember lookup against the Bot API, botIsAdmin/botHasPermission check the bot's own standing, membership() caches that lookup with event-driven invalidation, and guardOr answers a denied check instead of silently dropping the update.

install

pnpm add @yaebal/guards

usage

a membership-based guard (isAdmin, hasPermission, …) calls getChatMember — put it behind something narrower than "every update" so it isn't called on every message in the chat:

import { and, command } from "@yaebal/filters";
import { isAdmin } from "@yaebal/guards";

// getChatMember only runs for an actual /ban, not for every message that passes through
bot.filter(and(command("ban"), isAdmin), banHandler);

bot.guard() still works the same way — it's just as unconditional as any other bot.guard() call, so reach for it once you're already narrowed down (e.g. inside a sub-composer mounted only under /admin), not as the very first thing in the chain:

import { isAdmin, isGroup } from "@yaebal/guards";

const adminOnly = new Composer().guard(isGroup).guard(isAdmin);
adminOnly.command("ban", banHandler);
adminOnly.command("mute", muteHandler);

caching: membership()

every guard here also works without any setup — falling back to a direct getChatMember call. install membership() to cache that lookup per (chat, user), so a burst of commands from the same admin doesn't cost a burst of API calls:

import { isAdmin, membership } from "@yaebal/guards";

bot.install(membership({ ttl: 60_000 }));
bot.filter(and(command("ban"), isAdmin), banHandler); // cached for 60s per (chat, user)

cached entries are also dropped the instant telegram reports a real membership change (chat_member/my_chat_member) — a fresh promotion or demotion is never served stale, which a plain TTL alone can't guarantee. pass an existing @yaebal/cache client via { cache } to share one cache across plugins, or read membership().cache to pre-warm/inspect it directly.

answering a denial: guardOr

bot.guard() drops a failing update silently — right for background filtering, wrong for a user-facing command, where /ban from a non-admin should get a reply, not silence:

import { guardOr, isAdmin } from "@yaebal/guards";

bot.use(guardOr(isAdmin, (ctx) => ctx.reply("admins only"))).command("ban", banHandler);

anonymous admins & linked channels

an admin/owner posting with "hide my identity" on arrives with from set to GroupAnonymousBot and no user id bots can look up — a plain getChatMember on that from.id would just 400. every guard here already handles it:

  • isAdmin/isGroup/… treat an anonymous poster as at least an administrator, without an api call — telegram guarantees that much.
  • isOwner denies an anonymous poster: telegram never says whether they're the owner or "just" an administrator, and granting owner-only access on a guess would be wrong half the time.
  • hasMembership(...) grants an anonymous poster only when it would be correct under either possibility — i.e. only when both "creator" and "administrator" are in the accepted list.
  • hasPermission/hasAnyPermission/hasAllPermissions deny an anonymous poster by default (their actual flags are unknowable), unless you opt in with { allowAnonymous: true }.

isAnonymousAdmin(ctx) and fromLinkedChannel(ctx) (an automatic forward from a channel linked to this group — a different, unrelated case that also sets ctx.senderChat) are exported directly if you need to branch on either yourself.

checking the bot's own permissions

botIsAdmin/botHasPermission(permission) mirror isAdmin/hasPermission, but for the bot itself (ctx.me) — check before an action that needs standing, e.g. before pinChatMessage:

import { botHasPermission } from "@yaebal/guards";

bot.filter(and(command("pin"), botHasPermission("can_pin_messages")), (ctx) =>
	ctx.reply("I don't have permission to pin messages here."),
);

ctx.me is only known once the bot has resolved its own identity (long polling fills it in after getMe); until then these deny.

composes with @yaebal/filters

every guard here also fits @yaebal/filters' Filter<Context> shape, so it composes with and/or/not too:

import { and } from "@yaebal/filters";
import { isAdmin, isGroup } from "@yaebal/guards";

bot.filter(and(isGroup, isAdmin), (ctx) => ctx.reply("welcome, admin"));

api

| export | signature | description | | --- | --- | --- | | isPrivate | <C extends Context>(ctx: C) => ctx is C & { chat: Chat & { type: "private" } } | chat is a private (1:1) chat | | isGroup | <C extends Context>(ctx: C) => ctx is C & { chat: Chat & { type: "group" \| "supergroup" } } | chat is a group or supergroup | | isAdmin | (ctx: Context) => Promise<boolean> | chat owner, administrator, or an anonymous poster | | isOwner | (ctx: Context) => Promise<boolean> | specifically the chat's owner (anonymous poster denied) | | hasMembership(...statuses) | (...statuses: ChatMemberStatus[]) => (ctx: Context) => Promise<boolean> | current status is one of statuses | | hasPermission(permission, opts?) | (permission: ChatPermission, opts?: PermissionOptions) => (ctx: Context) => Promise<boolean> | owner always passes; administrator passes when the flag is set | | hasAnyPermission(permissions, opts?) | (permissions: ChatPermission[], opts?: PermissionOptions) => (ctx: Context) => Promise<boolean> | owner always passes; administrator passes when any flag is set | | hasAllPermissions(permissions, opts?) | (permissions: ChatPermission[], opts?: PermissionOptions) => (ctx: Context) => Promise<boolean> | owner always passes; administrator passes when every flag is set | | botIsAdmin | (ctx: Context) => Promise<boolean> | the bot itself is owner/administrator | | botHasPermission(permission) | (permission: ChatPermission) => (ctx: Context) => Promise<boolean> | the bot itself has the flag set | | isAnonymousAdmin | (ctx: Context) => boolean | update is an anonymous admin/owner post | | fromLinkedChannel | (ctx: Context) => boolean | update is an automatic forward from a linked channel | | resolveMember | (ctx: Context, userId?: number) => Promise<MemberResolution> | the low-level lookup every predicate above is built on | | membership(options?) | (options?: MembershipOptions) => MembershipPlugin | cache getChatMember lookups, invalidated by chat_member/my_chat_member | | guardOr(predicate, onDeny) | <C extends Context>(predicate, onDeny) => Middleware<C> | gate like guard(), but answer a denial instead of dropping it | | asGuard(filter) | (filter: Filter<Context, Add>) => <C extends Context>(ctx: C) => ctx is C & Add | adapt a sync, non-staging @yaebal/filters predicate into a guard |

ChatPermission

derived from ChatMemberAdministrator's can_* flags (minus can_be_edited and is_anonymous), so it always matches the current Bot API schema: can_manage_chat, can_delete_messages, can_manage_video_chats, can_restrict_members, can_promote_members, can_change_info, can_invite_users, can_post_stories, can_edit_stories, can_delete_stories, can_post_messages, can_edit_messages, can_pin_messages, can_manage_topics, can_manage_direct_messages, can_manage_tags.

behavior

a getChatMember lookup that fails as telegram saying "no" (user not found, bot isn't in the chat, …) denies access. anything else — a network failure, a malformed response — throws through the predicate instead of masquerading as a deny; a permission check that fails silently and looks like a permissions bug forever is worse than one that's loud about an actual outage. an update's user is never treated as privileged by default.

asGuard only accepts synchronous, non-staging filters — passing an async filter or one that stages bag data (command(), regex(), …) throws immediately rather than silently misbehaving (an async filter always denies as a guard predicate; staged data would be silently dropped). reach for bot.filter() for those instead.

testing

import { createTestEnv } from "@yaebal/test";

const env = createTestEnv(bot);
env.onApi("getChatMember", {
	status: "administrator",
	user: { id: 1, is_bot: false, first_name: "admin" },
	can_restrict_members: true,
});

chatMemberUpdate/myChatMemberUpdate (also from @yaebal/test) simulate the promotion/demotion events membership() listens for.


part of yaebal — a type-safe, runtime-agnostic Telegram Bot API framework. MIT.