← back to Norma

agents/shared/credentials.js

139 lines

/**
 * Norma Agent Credential Loader
 *
 * Loads platform credentials from the social_accounts DB table.
 * Falls back to process.env if DB has no credentials.
 *
 * Usage:
 *   const { loadCredentials } = require('../shared/credentials');
 *   const creds = await loadCredentials('twitter');
 *   // creds.TWITTER_BEARER_TOKEN, etc.
 */

const { query } = require('./db');

// In-memory cache with 5-minute TTL
const _cache = {};
const CACHE_TTL_MS = 5 * 60 * 1000;

/**
 * Load credentials for a social platform from DB, falling back to env vars.
 *
 * @param {'twitter'|'bluesky'|'facebook'|'instagram'} platform
 * @returns {Promise<Object|null>} Platform-specific credential object keyed by env var names
 */
async function loadCredentials(platform) {
  // Check cache
  const cached = _cache[platform];
  if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
    return cached.data;
  }

  try {
    const result = await query(
      `SELECT access_token, refresh_token, token_expires_at,
              meta_page_id, meta_ig_user_id, credentials_extra, status
       FROM social_accounts
       WHERE platform = $1 AND status != 'disconnected'
       LIMIT 1`,
      [platform],
    );

    if (result.rows.length === 0) {
      return _fallbackToEnv(platform);
    }

    const row = result.rows[0];
    const extra = row.credentials_extra || {};
    let creds = null;

    switch (platform) {
      case 'twitter':
        creds = {
          TWITTER_BEARER_TOKEN: row.access_token || process.env.TWITTER_BEARER_TOKEN || '',
          TWITTER_API_KEY: extra.api_key || process.env.TWITTER_API_KEY || '',
          TWITTER_API_SECRET: extra.api_secret || process.env.TWITTER_API_SECRET || '',
          TWITTER_ACCESS_TOKEN: extra.access_token || process.env.TWITTER_ACCESS_TOKEN || '',
          TWITTER_ACCESS_SECRET: extra.access_secret || process.env.TWITTER_ACCESS_SECRET || '',
        };
        break;
      case 'bluesky':
        creds = {
          BSKY_HANDLE: row.access_token || process.env.BSKY_HANDLE || '',
          BSKY_PASSWORD: row.refresh_token || process.env.BSKY_PASSWORD || '',
          BSKY_SERVICE: extra.service_url || process.env.BSKY_SERVICE || 'https://bsky.social',
        };
        break;
      case 'facebook':
        creds = {
          FB_PAGE_ID: row.meta_page_id || process.env.FB_PAGE_ID || '',
          FB_PAGE_ACCESS_TOKEN: row.access_token || process.env.FB_PAGE_ACCESS_TOKEN || '',
        };
        break;
      case 'instagram':
        creds = {
          IG_USER_ID: row.meta_ig_user_id || process.env.IG_USER_ID || '',
          IG_ACCESS_TOKEN: row.access_token || process.env.IG_ACCESS_TOKEN || '',
        };
        break;
      default:
        return null;
    }

    // Cache result
    _cache[platform] = { data: creds, ts: Date.now() };
    return creds;
  } catch (err) {
    console.error(`[credentials] Failed to load ${platform} from DB, falling back to env:`, err.message);
    return _fallbackToEnv(platform);
  }
}

/**
 * Fallback: build credential object from process.env only.
 */
function _fallbackToEnv(platform) {
  switch (platform) {
    case 'twitter':
      return {
        TWITTER_BEARER_TOKEN: process.env.TWITTER_BEARER_TOKEN || '',
        TWITTER_API_KEY: process.env.TWITTER_API_KEY || '',
        TWITTER_API_SECRET: process.env.TWITTER_API_SECRET || '',
        TWITTER_ACCESS_TOKEN: process.env.TWITTER_ACCESS_TOKEN || '',
        TWITTER_ACCESS_SECRET: process.env.TWITTER_ACCESS_SECRET || '',
      };
    case 'bluesky':
      return {
        BSKY_HANDLE: process.env.BSKY_HANDLE || '',
        BSKY_PASSWORD: process.env.BSKY_PASSWORD || '',
        BSKY_SERVICE: process.env.BSKY_SERVICE || 'https://bsky.social',
      };
    case 'facebook':
      return {
        FB_PAGE_ID: process.env.FB_PAGE_ID || '',
        FB_PAGE_ACCESS_TOKEN: process.env.FB_PAGE_ACCESS_TOKEN || '',
      };
    case 'instagram':
      return {
        IG_USER_ID: process.env.IG_USER_ID || '',
        IG_ACCESS_TOKEN: process.env.IG_ACCESS_TOKEN || '',
      };
    default:
      return null;
  }
}

/**
 * Clear the in-memory credential cache for a platform (or all).
 * Useful after credentials are updated via the UI.
 */
function clearCache(platform) {
  if (platform) {
    delete _cache[platform];
  } else {
    for (const key of Object.keys(_cache)) delete _cache[key];
  }
}

module.exports = { loadCredentials, clearCache };