← back to Goodquestion Ai

scripts/youtube-refresh-token.cjs

132 lines

#!/usr/bin/env node
/**
 * YouTube OAuth Token Auto-Refresh
 *
 * Refreshes the YouTube access token using the stored refresh_token.
 * Run via cron every 6 days to prevent token expiry.
 *
 * Cron: 0 6 [every-6-days] node /root/Projects/goodquestion-ai/scripts/youtube-refresh-token.cjs
 *
 * If refresh fails (invalid_grant), sends Slack alert for manual re-auth.
 */

const fs = require('fs');
const path = require('path');
const https = require('https');

require('dotenv').config({ path: path.join(__dirname, '..', '.env') });

const TOKEN_PATH = path.join(__dirname, '..', '.youtube-token.json');
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;

function log(msg) {
  const ts = new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles' });
  console.log(`[${ts} PT] ${msg}`);
}

async function sendSlackAlert(message) {
  if (!SLACK_WEBHOOK) {
    log('No SLACK_WEBHOOK_URL configured — skipping alert');
    return;
  }

  return new Promise((resolve) => {
    const payload = JSON.stringify({ text: message });
    const urlObj = new URL(SLACK_WEBHOOK);

    const req = https.request({
      hostname: urlObj.hostname,
      path: urlObj.pathname,
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
    }, (res) => {
      res.on('data', () => {});
      res.on('end', resolve);
    });

    req.on('error', () => resolve());
    req.write(payload);
    req.end();
  });
}

async function refreshToken() {
  log('YouTube Token Auto-Refresh starting...');

  // Load current tokens
  let tokens;
  try {
    tokens = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf-8'));
  } catch (err) {
    log(`ERROR: Cannot read token file: ${err.message}`);
    await sendSlackAlert('🔴 YouTube token file missing or corrupted. Manual re-auth needed: `node scripts/youtube-auth.cjs`');
    process.exit(1);
  }

  if (!tokens.refresh_token) {
    log('ERROR: No refresh_token in token file');
    await sendSlackAlert('🔴 YouTube refresh_token missing. Manual re-auth needed: `node scripts/youtube-auth.cjs`');
    process.exit(1);
  }

  // Check current expiry
  const currentExpiry = tokens.expiry_date ? new Date(tokens.expiry_date) : null;
  const now = new Date();
  log(`Current token expires: ${currentExpiry ? currentExpiry.toISOString() : 'unknown'}`);
  log(`Time until expiry: ${currentExpiry ? Math.round((currentExpiry - now) / 3600000) + 'h' : 'unknown'}`);

  // Use googleapis to refresh
  let google;
  try {
    google = require(path.join(__dirname, '..', 'node_modules', 'googleapis')).google;
  } catch {
    log('ERROR: googleapis not installed');
    process.exit(1);
  }

  const oauth2 = new google.auth.OAuth2(
    process.env.YOUTUBE_CLIENT_ID,
    process.env.YOUTUBE_CLIENT_SECRET,
    process.env.YOUTUBE_REDIRECT_URI || 'http://localhost:3456/oauth/callback'
  );

  oauth2.setCredentials(tokens);

  try {
    const result = await oauth2.refreshAccessToken();
    const newTokens = result.credentials;

    // Save refreshed tokens
    fs.writeFileSync(TOKEN_PATH, JSON.stringify(newTokens, null, 2));

    const newExpiry = new Date(newTokens.expiry_date);
    log(`SUCCESS — Token refreshed!`);
    log(`New expiry: ${newExpiry.toISOString()}`);
    log(`Next refresh due before: ${new Date(Date.now() + 6 * 86400000).toISOString()}`);

    await sendSlackAlert(`✅ YouTube token auto-refreshed. New expiry: ${newExpiry.toLocaleString('en-US', { timeZone: 'America/Los_Angeles' })} PT`);

  } catch (err) {
    log(`REFRESH FAILED: ${err.message}`);

    if (err.message.includes('invalid_grant')) {
      log('Refresh token expired — manual re-auth required');
      await sendSlackAlert(
        '🔴 YouTube refresh token EXPIRED. Manual re-auth needed:\n' +
        '```\ncd /root/Projects/goodquestion-ai\n' +
        'node scripts/youtube-auth.cjs\n```\n' +
        'Then open the auth URL in a browser and complete the OAuth flow.'
      );
    } else {
      await sendSlackAlert(`🟡 YouTube token refresh failed: ${err.message}. Will retry next cycle.`);
    }

    process.exit(1);
  }
}

refreshToken().catch(err => {
  log(`FATAL: ${err.message}`);
  process.exit(1);
});