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

@graygate/bot-sdk

v0.1.0

Published

Runtime SDK for graygate bots — webhook receiver, signature verification, end-to-end encryption.

Readme

@graygate/bot-sdk

Runtime SDK for graygate bots. Node, TypeScript.

A bot on graygate is an account run by a machine. It holds its own RSA key pair, joins rooms by invitation, and reads and writes end-to-end encrypted messages. The server relays ciphertext and cannot read any of it — which is why this runtime, and not graygate, holds the keys.

Start here: https://graygate.app/bots/ — what a bot is, what it may and may not do, and the five steps from creating one in the app to answering a message.

Back up your state directory. stateDir holds the bot's RSA private key and the room keys it has been given, as plain files. Neither the server nor the phone of the person who created the bot has a copy. Lose it and the past messages of those rooms stay closed forever — there is no room-key backup, by design. If you lose only the private key you can replace it with PUT /bot/public-key, but the room keys come back only when each room admin next rotates.

Install

Until this package is on npm, install it from the repository:

npm install github:graygate/bot-sdk

Node 20 or newer.

Use

import { Bot } from '@graygate/bot-sdk';

const bot = new Bot({
  token: process.env.GRAYGATE_BOT_TOKEN!,   // gg1.<botId>.<secret>, from the app
  stateDir: './state',                      // private key, room keys, handled ids
  webhook: { port: 8443, path: '/hook', publicUrl: 'https://bot.example.com/hook' },
  autoAcceptInvites: true,
});

bot.command('start', (ctx) => ctx.reply('Hello.'));
bot.on('message', async (ctx) => { if (ctx.text === 'ping') await ctx.reply('pong'); });
bot.on('join', async (ctx) => { await ctx.reply('Welcome.'); });
bot.on('invite', async (invite) => invite.accept());

await bot.start();

start() activates the bot if this is its first run (generating the key pair locally), serves the webhook, and registers it. It registers on every start on purpose: there is no polling, so re-registering is what makes the server replay everything that piled up while the bot was down, oldest first.

Options

| Option | | |---|---| | token | gg1.<botId>.<secret>, shown once in the app when the bot is created. Pass it through the environment; never write it to a file | | stateDir | Private key, room keys by version, and the ids already handled. Back it up | | webhook.publicUrl | The public https address the server will post to. https and port 443 only; private, loopback and link-local addresses are refused | | webhook.port / webhook.host | Where the SDK listens. Bind to 127.0.0.1 when a reverse proxy terminates TLS | | autoAcceptInvites | Default true. Set false and decide inside bot.on('invite') | | origin | Server origin. Defaults to https://server.graygate.app |

Handlers

  • bot.command(name, ctx) — a command is an ordinary text message that starts with /. In a room with more than one bot, people write /start@your_bot; a command addressed to another bot is ignored for you.
  • bot.on('message', ctx) — ctx.text is set for text bodies, ctx.body always carries the decrypted body, ctx.reply(text) answers in the same room.
  • bot.on('join', ctx) — someone joined a room the bot is in. Useful for a greeting.
  • bot.on('invite', invite) — invite.accept() or invite.decline().

A handler that throws makes the SDK answer the webhook with a non-2xx status, and the server sends the batch again after a backoff. That is the intended way to say "I could not handle this yet".

What you take on

  • A public https address is required. The server checks it when you register and again on every send, so an address that later resolves to a private range stops receiving. Redirects are not followed. Running on a laptop means running a tunnel.
  • Your bot reads everything in its rooms. Room keys are shared, so there is no privacy mode: the component that would filter is the server, and the server sees only ciphertext. Members are told — the invite raises a confirmation dialog, a warning line appears when the bot joins, and a BOT badge follows it. What your runtime logs, stores or forwards is your responsibility, and rooms that set an auto-delete period expect you to honour it. The default state keeps no messages.
  • Media handed to you is unverified. Phones run every incoming file through a validation pipeline before anything renders it. This SDK does not: it decrypts bytes and gives them to you. Do not pass them straight to an image decoder, a file path or a system handler.
  • If the token leaks, issue a new one in the app and use Leave all rooms. Both take effect at once, but neither takes back room keys already copied — only a room admin can, by rotating. Tell them.

Webhook contract

Every request from the server carries a signature. Verify it before parsing the body; the SDK does this for you and answers 401 without reading anything it cannot authenticate.

header:  x-graygate-signature: t=<unix seconds>,v1=<hex>
key:     HKDF-SHA256(ikm = sha256(the token's secret part), salt = none,
                     info = "graygate:bot-webhook-v1", length = 32)
v1:      HMAC-SHA256(key, "<t>." + <raw request body>)
verify:  constant-time compare, and reject |now - t| > 300 seconds as a replay

The signing key comes from the sha256 of the token's secret part rather than the secret itself because the server stores only that hash — it never keeps the secret in the clear. Reissuing the token changes the hash, so the signing key changes with it.

A 2xx response is the acknowledgement: it deletes the server's queued copy. Delivery is at-least-once, so drop repeats by item id (the SDK does). Batches carry up to 100 items, requests time out after 3 seconds, and after 20 consecutive failures the server switches your webhook off and messages only wait in the queue, for 14 days.

Other languages

This is one implementation, not the contract. The REST endpoints, the signature above, and the crypto parameters (AES-256-CBC with encrypt-then-HMAC-SHA256, RSA-OAEP room-key envelopes, all via graygate-cypher) are enough to write a runtime in any language. The wire types in src/wire/ are the same ones the app and the server use.

Contributing

The sources here are generated from the graygate monorepo, which is where fixes land. Open an issue describing the problem — pull requests against generated files cannot be merged directly, but the change will be made upstream and land here on the next release.

License

MIT. See LICENSE.