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-authRegister 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 a member
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.
Use the link
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.
When a link ends
| Who ends it | What 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.cloud | The tokens stop working at once. The SDK notices, removes the link and calls onUnlinked(key, { reason: "revoked", user }). |
| Nobody uses it for 60 days | The 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 nowCastyr 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.
Where links are kept
| Store | Use 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 own | Several shards or servers: an object with get, set, delete and keys over your database. |
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
| Method | Does |
|---|---|
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.