← 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 };