import { WebClient } from "@slack/web-api";
import pino from "pino";
import type { InstallStore, SlackInstallation } from "./installStore.js";
const log = pino({ level: process.env.LOG_LEVEL ?? "info" });
const SLACK_CLIENT_ID = process.env.SLACK_CLIENT_ID!;
const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET!;
// Refresh token if it expires within 1 hour
const REFRESH_BUFFER_MS = 60 * 60 * 1000;
async function refreshTokenIfNeeded(install: SlackInstallation, store: InstallStore): Promise<string> {
// If no expiration or refresh token, return current token
if (!install.expires_at || !install.refresh_token) {
return install.bot_access_token;
}
// Check if token is expired or will expire soon
const now = Date.now();
if (now + REFRESH_BUFFER_MS < install.expires_at) {
return install.bot_access_token; // Token is still valid
}
log.info({ team_id: install.team_id }, "Refreshing expired Slack token");
try {
// Refresh the token using oauth.v2.access with refresh_token grant
const form = new URLSearchParams();
form.set("client_id", SLACK_CLIENT_ID);
form.set("client_secret", SLACK_CLIENT_SECRET);
form.set("grant_type", "refresh_token");
form.set("refresh_token", install.refresh_token);
const response = await fetch("https://slack.com/api/oauth.v2.access", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: form
});
const data: any = await response.json();
if (!data.ok) {
log.error({ team_id: install.team_id, error: data.error }, "Failed to refresh Slack token");
throw new Error(`Token refresh failed: ${data.error}`);
}
// Update the installation with new tokens
const updatedInstall: SlackInstallation = {
...install,
bot_access_token: data.access_token,
refresh_token: data.refresh_token ?? install.refresh_token,
expires_at: data.expires_in ? Date.now() + Number(data.expires_in) * 1000 : null,
scope: data.scope ?? install.scope
};
await store.upsert(updatedInstall);
log.info({ team_id: install.team_id }, "Slack token refreshed successfully");
return updatedInstall.bot_access_token;
} catch (err) {
log.error({ err, team_id: install.team_id }, "Failed to refresh token");
// Return the old token and let Slack API return an error if it's truly expired
return install.bot_access_token;
}
}
export async function getSlackClientForTeam(store: InstallStore, teamId: string): Promise<WebClient> {
const install = await store.getByTeamId(teamId);
if (!install) {
throw new Error(`No Slack installation found for team_id=${teamId}. Please install the app first.`);
}
// Refresh token if needed (handles token rotation)
const token = await refreshTokenIfNeeded(install, store);
return new WebClient(token);
}