← back to Ventura Claw
server/lib/oauth-refresh.js
60 lines
// VenturaClaw — OAuth refresh-token rotation.
// Called before every connector action to auto-refresh expired access tokens.
// Returns possibly-updated creds (caller saves if changed).
const SAFETY_WINDOW_MS = 60_000; // refresh if expiring within 60 seconds
async function refreshIfNeeded(vendor, creds, provider, app) {
if (!creds || creds._via !== "oauth") return { creds, refreshed: false }; // not oauth-managed
if (!creds.refresh_token || !creds.expires_at) return { creds, refreshed: false };
const expiresAt = new Date(creds.expires_at).getTime();
if (!Number.isFinite(expiresAt)) return { creds, refreshed: false };
if (expiresAt > Date.now() + SAFETY_WINDOW_MS) return { creds, refreshed: false }; // still fresh
if (!provider?.token_url || !app?.client_id) return { creds, refreshed: false, reason: "no_provider_or_app" };
const body = new URLSearchParams({
client_id: app.client_id,
client_secret: app.client_secret,
refresh_token: creds.refresh_token,
grant_type: "refresh_token"
});
let resp, json;
try {
resp = await fetch(provider.token_url, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
body,
signal: AbortSignal.timeout(10_000)
});
json = await resp.json();
} catch (e) {
return {
creds: { ...creds, _refresh_failed: e.message, _refresh_failed_at: new Date().toISOString() },
refreshed: false, error: e.message
};
}
if (!resp.ok || !json.access_token) {
return {
creds: { ...creds, _refresh_failed: json.error || `http ${resp.status}`, _refresh_failed_at: new Date().toISOString() },
refreshed: false, error: json.error || `http ${resp.status}`
};
}
const updated = {
...creds,
access_token: json.access_token,
refresh_token: json.refresh_token || creds.refresh_token, // some vendors don't issue new RT
token_type: json.token_type || creds.token_type || "Bearer",
scope: json.scope || creds.scope,
expires_at: json.expires_in ? new Date(Date.now() + json.expires_in * 1000).toISOString() : creds.expires_at,
_refreshed_at: new Date().toISOString(),
_refresh_failed: undefined,
_refresh_failed_at: undefined,
_via: "oauth"
};
return { creds: updated, refreshed: true };
}
module.exports = { refreshIfNeeded };