santifer/career-ops · error · Error

gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL

Error message

gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN in .env

What it means

The Gmail ingest plugin requires three OAuth credentials to authenticate against the Gmail API: a client ID, client secret, and a long-lived refresh token. This error fires at the very start of ingest() when any of the three is absent or empty in ctx.env (populated from .env). The check is a flat OR across all three, so the message names all of them regardless of which one is actually missing.

Source

Thrown at plugins/gmail/index.mjs:77

}

function saveProcessedIds(ids) {
  try {
    mkdirSync('data', { recursive: true });
    writeFileSync(STATE_PATH, JSON.stringify({ processed_message_ids: [...ids] }, null, 2), 'utf-8');
  } catch (err) {
    console.warn(`gmail: could not persist processed-id state — ${err.message}`);
  }
}

/** @type {{ ingest: (ctx: any) => Promise<object[]> }} */
export default {
  async ingest(ctx) {
    const clientId = ctx?.env?.GMAIL_CLIENT_ID;
    const clientSecret = ctx?.env?.GMAIL_CLIENT_SECRET;
    const refreshToken = ctx?.env?.GMAIL_REFRESH_TOKEN;
    if (!clientId || !clientSecret || !refreshToken) {
      throw new Error('gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN in .env');
    }

    const label = ctx?.settings?.label || 'Job Leads';
    const daysBack = Number(ctx?.settings?.days_back ?? 7);
    if (!Number.isInteger(daysBack) || daysBack <= 0) {
      throw new Error(`gmail: invalid days_back "${ctx?.settings?.days_back}" (must be a positive integer)`);
    }

    const token = await getAccessToken({ clientId, clientSecret, refreshToken }, ctx.fetch);
    const auth = { Authorization: `Bearer ${token}` };
    const query = `label:"${label}" newer_than:${daysBack}d`;
    ctx.log(`gmail: querying ${query}`);

    // List message ids (paginated). ctx.fetch throws on a non-2xx (with the body
    // in the message), so a failed page surfaces a clear error.
    const messages = [];
    let pageToken = null;
    do {

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Add all three vars to .env: GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN — get them from a Google Cloud Console OAuth 2.0 client (Desktop/app type) where Gmail API is enabled and the user has authorized the https://mail.google.com/ scope to obtain the refresh token.
  2. Confirm the engine is loading that .env — the plugin reads ctx.env, so the vars must reach the plugin context, not just process.env of a different process.
  3. Re-run `node plugins.mjs run gmail`.

Example fix

# before (.env)
GMAIL_CLIENT_ID=
GMAIL_CLIENT_SECRET=xxxxx
GMAIL_REFRESH_TOKEN=yyyyy

# after (.env)
GMAIL_CLIENT_ID=123456-abc.apps.googleusercontent.com
GMAIL_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxx
GMAIL_REFRESH_TOKEN=1//0eXXXXXXXXXXXXXXXX
Defensive patterns

Strategy: validation

Validate before calling

// Before calling the plugin's ingest, verify the env is populated.
import { existsSync, readFileSync } from 'fs';
function gmailEnvReady(env = process.env) {
  const need = ['GMAIL_CLIENT_ID', 'GMAIL_CLIENT_SECRET', 'GMAIL_REFRESH_TOKEN'];
  const missing = need.filter((k) => !env[k]);
  if (missing.length) {
    return { ready: false, missing };
  }
  return { ready: true, missing: [] };
}
const { ready, missing } = gmailEnvReady();
if (!ready) console.error(`gmail not configured; set in .env: ${missing.join(', ')}`);

Type guard

// Narrow plugin ctx.env before passing to ingest.
/** @typedef {{ GMAIL_CLIENT_ID: string, GMAIL_CLIENT_SECRET: string, GMAIL_REFRESH_TOKEN: string }} GmailEnv */
/** @param {unknown} e
 *  @returns {e is GmailEnv} */
function isGmailEnv(e) {
  const env = /** @type {Record<string, unknown>} */ (e ?? {});
  return typeof env.GMAIL_CLIENT_ID === 'string' && env.GMAIL_CLIENT_ID.length > 0
    && typeof env.GMAIL_CLIENT_SECRET === 'string' && env.GMAIL_CLIENT_SECRET.length > 0
    && typeof env.GMAIL_REFRESH_TOKEN === 'string' && env.GMAIL_REFRESH_TOKEN.length > 0;
}

Try / catch

try {
  const jobs = await gmailPlugin.ingest(ctx);
} catch (err) {
  if (err instanceof Error && err.message.startsWith('gmail: missing GMAIL_CLIENT_ID')) {
    console.warn('Skipping gmail ingest — configure credentials in .env');
    // non-fatal: continue without gmail leads
  } else throw err;
}

Prevention

When it happens

Trigger: Calling `node plugins.mjs run gmail` (or any engine entry that drives the gmail plugin's ingest hook) when .env lacks one or more of GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN. Also fires if the vars are set but empty (e.g. `GMAIL_CLIENT_ID=`), since the guard is falsy-checking (!clientId || ...).

Common situations: First-time plugin setup (OAuth credentials never created in Google Cloud Console); .env not loaded by the runner; credentials added to a different .env than the one the engine reads; refresh token revoked or never generated (OAuth app in testing without a granted refresh token).

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/5660e633ace70ba5. Report an issue: GitHub.