CastyrDocs

Bot SDK: @castyr/bots-auth

Link a bot to Castyr accounts in Node.js. Links are saved, tokens renew themselves, and you hear about sign-outs.

  • No dependencies. Node 18+. ESM and CommonJS, with TypeScript types.
  • Handles polling, slow-downs, expired codes and brief network errors.
  • Owners approve once: links survive restarts and renew themselves.
npm install @castyr/bots-auth

Register a Bot app under Developers and copy its client_id. Bot apps are public clients: there is no secret to keep, and they can refresh their tokens.

link.js
import { CastyrBotAuth, FileTokenStore, ActivationDeniedError, ActivationExpiredError } from "@castyr/bots-auth";

const castyr = new CastyrBotAuth({
  clientId: process.env.CASTYR_CLIENT_ID,
  store: new FileTokenStore("castyr-links.json"), // survives restarts
});

try {
  await castyr.activate({
    key: memberId,                // your id for this link, e.g. a Discord user id
    platformName: "Helper Bot",   // shown to the owner when they approve
    scope: "identify",
    onCode: ({ userCode, verificationUriComplete, confirmationPhrase }) =>
      sendPrivately(memberId, `Open ${verificationUriComplete}, check the code is ${userCode}, pick "${confirmationPhrase}".`),
  });
  const { user } = await castyr.getLink(memberId);
  console.log(`Linked to @${user.preferred_username}`);
} catch (error) {
  if (error instanceof ActivationDeniedError) { /* denied, or the wrong phrase was picked */ }
  else if (error instanceof ActivationExpiredError) { /* nobody approved within 10 minutes */ }
  else throw error;
}

Show the code only to the member linking

Use a DM or a message only they can see, and always show confirmationPhrase next to the code. They must pick that phrase out of three to approve; a wrong pick cancels the request.

const token = await castyr.getAccessToken(memberId); // null if not linked
if (token) {
  const profile = await castyr.getUser(token);       // { sub, name, preferred_username, picture }
}

getAccessToken always returns a usable token. When the current one has under 5 minutes left, it renews it and saves the result. Refresh tokens rotate on every renewal, so the SDK makes sure only one renewal per link runs at a time. Links used or checked at least every 60 days never need approving again.

Who ends itWhat happens
The bot calls unlink(key)The tokens are revoked, and the bot disappears from the owner's Devices and Connected apps pages.
The owner clicks Sign out on sso.castyr.cloudThe tokens stop working at once. The SDK notices, removes the link and calls onUnlinked(key, { reason: "revoked", user }).
Nobody uses it for 60 daysThe refresh token expires; onUnlinked is called with reason: "expired".
const castyr = new CastyrBotAuth({
  clientId: process.env.CASTYR_CLIENT_ID,
  store: new FileTokenStore("castyr-links.json"),
  onUnlinked: (memberId, { reason, user }) => notify(memberId, "You're no longer connected to Castyr."),
});
castyr.startAutoRefresh();                            // renews tokens and checks every link every 5 minutes
const stillLinked = await castyr.verifyLink(memberId); // ask Castyr right now

Castyr can't push to your bot, so a sign-out on the site is noticed on the next check: within checkEverySeconds (default 300) in the background, or immediately when you call verifyLink.

StoreUse it for
MemoryTokenStore (default)Trying things out. Lost when the bot stops.
new FileTokenStore(path)One bot process. A JSON file only the bot's user can read, replaced atomically on every change.
Your ownSeveral shards or servers: an object with get, set, delete and keys over your database.
A store over any key-value database (sketch)
const store = {
  get: async (key) => revive(await db.get(`castyr:${key}`)),
  set: (key, link) => db.set(`castyr:${key}`, link),
  delete: (key) => db.delete(`castyr:${key}`),
  keys: () => db.keys("castyr:*").then((ks) => ks.map((k) => k.slice(7))),
};
// Dates come back from JSON as strings: revive link.linkedAt and link.tokens.expiresAt with new Date(...).

Stored links contain tokens: keep the file or table private, and out of git.

API

new CastyrBotAuth(options)

Prop

Type

Methods

MethodDoes
activate({ key?, platformName, platformDescription?, platformIcon?, scope?, onCode })Get a code, show it with onCode, wait for approval, save the link.
startActivation(options) / waitForAuthorization(activation, { key? })The same, in two steps.
getAccessToken(key)A usable token, renewed if needed; null if not linked.
getLink(key){ tokens, user, linkedAt }.
verifyLink(key)Ask Castyr whether the link still works.
unlink(key)Revoke and forget the link.
startAutoRefresh({ everySeconds?, checkEverySeconds?, onError? })Keep every link renewed and checked; returns stop().
getUser(accessToken)The owner's profile (claims).

Errors from Castyr are CastyrAuthError with an OAuth code (see Errors and limits); ActivationDeniedError and ActivationExpiredError cover the owner's side.

Next: build a Discord bot with it.

On this page