enerthya.dev-oauth2
v1.0.0
Published
Discord OAuth2 flow for the Enerthya ecosystem. Inspired by Loritta's discord-oauth2 module.
Maintainers
Readme
enerthya.dev-oauth2
Discord OAuth2 flow for the Enerthya dashboard. Inspired by Loritta's
discord-oauth2module (LorittaBot). Part of the Enerthya package ecosystem.
Features
- Full Discord OAuth2 lifecycle: exchange, refresh, revoke, user data, user guilds
createFromEnv()— readsCLIENT_ID,CLIENT_SECRET,CALLBACK_URLfrom.envOAuth2Error— typed errors withcode(UPPER_SNAKE) and HTTPstatus- Zero dependencies — uses native
fetch(Node.js 18+) - Zero hardcoded credentials
Installation
npm install enerthya.dev-oauth2Quick Start
const { createFromEnv, OAuth2Error } = require("enerthya.dev-oauth2");
const oauth = createFromEnv(); // reads CLIENT_ID, CLIENT_SECRET, CALLBACK_URL from .env
// Build the login URL
app.get("/auth/login", (req, res) => {
res.redirect(oauth.getAuthUrl());
});
// Handle callback
app.get("/auth/callback", async (req, res) => {
try {
const tokens = await oauth.exchangeCode(req.query.code);
const user = await oauth.getUserData(tokens.access_token);
req.session.tokens = tokens;
res.redirect("/dashboard");
} catch (err) {
if (err instanceof OAuth2Error) {
console.error(`[OAuth2] ${err.code} (${err.status}): ${err.message}`);
}
res.redirect("/?error=auth_failed");
}
});
// Logout
app.post("/auth/logout", async (req, res) => {
if (req.session.tokens?.access_token) {
await oauth.revokeToken(req.session.tokens.access_token).catch(() => {});
}
req.logout(() => res.json({ success: true }));
});API Reference
createFromEnv(overrides?) → EnerthyaOAuth2Client
Creates a client from environment variables. Required vars: CLIENT_ID, CLIENT_SECRET, CALLBACK_URL.
const oauth = createFromEnv();
// or with overrides:
const oauth = createFromEnv({ scopes: ["identify"] });new EnerthyaOAuth2Client(options)
const oauth = new EnerthyaOAuth2Client({
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
redirectUri: "https://your-site.com/auth/callback",
scopes: ["identify", "guilds"], // optional, defaults to ["identify", "guilds"]
});oauth.getAuthUrl(state?) → string
Builds the Discord authorization URL. Pass state for CSRF protection.
const url = oauth.getAuthUrl("random_csrf_token");
res.redirect(url);oauth.exchangeCode(code) → Promise<TokenResponse>
Trades an authorization code (from Discord's callback) for access + refresh tokens.
const tokens = await oauth.exchangeCode(req.query.code);
// { access_token, refresh_token, token_type, expires_in, scope }oauth.refreshToken(refreshToken) → Promise<TokenResponse>
Renews an expired access token.
const newTokens = await oauth.refreshToken(session.tokens.refresh_token);oauth.revokeToken(token) → Promise<void>
Revokes a token. Silently succeeds if already invalid (safe to call on logout always).
await oauth.revokeToken(session.tokens.access_token);oauth.getUserData(accessToken) → Promise<DiscordUser>
Fetches the user's Discord profile (/users/@me).
const user = await oauth.getUserData(tokens.access_token);
// { id, username, global_name, avatar, discriminator }oauth.getUserGuilds(accessToken) → Promise<PartialGuild[]>
Fetches the guilds the user belongs to. Requires guilds scope.
const guilds = await oauth.getUserGuilds(tokens.access_token);
const adminGuilds = guilds.filter(g => (g.permissions & 0x8) === 0x8);OAuth2Error
All methods throw OAuth2Error on failure.
try {
await oauth.exchangeCode(code);
} catch (err) {
if (err instanceof OAuth2Error) {
console.log(err.code); // e.g. "TOKEN_EXPIRED", "RATE_LIMITED", "NETWORK_ERROR"
console.log(err.status); // e.g. 401, 429 (null if no HTTP context)
console.log(err.message); // human-readable description
}
}Common error codes:
| Code | Meaning |
|------|---------|
| MISSING_CODE | exchangeCode() called without code |
| MISSING_REFRESH_TOKEN | refreshToken() called without token |
| MISSING_ACCESS_TOKEN | getUserData() / getUserGuilds() called without token |
| MISSING_TOKEN | revokeToken() called without token |
| TOKEN_EXPIRED | Discord returned 401 |
| RATE_LIMITED | Discord returned 429 |
| NETWORK_ERROR | Fetch failed (no connection) |
| REQUEST_TIMEOUT | Request exceeded timeout |
| DISCORD_API_ERROR | Other non-2xx response from Discord |
| MISSING_CLIENT_ID | Constructor missing clientId |
| MISSING_CLIENT_SECRET | Constructor missing clientSecret |
| MISSING_REDIRECT_URI | Constructor missing redirectUri |
License
MIT — Enerthya
