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