{"record":{"id":"5660e633ace70ba5","repo":"santifer/career-ops","slug":"gmail-missing-gmail-client-id-gmail-client-secr","errorCode":null,"errorMessage":"gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN in .env","messagePattern":"gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN in \\.env","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/gmail/index.mjs","lineNumber":77,"sourceCode":"}\n\nfunction saveProcessedIds(ids) {\n  try {\n    mkdirSync('data', { recursive: true });\n    writeFileSync(STATE_PATH, JSON.stringify({ processed_message_ids: [...ids] }, null, 2), 'utf-8');\n  } catch (err) {\n    console.warn(`gmail: could not persist processed-id state — ${err.message}`);\n  }\n}\n\n/** @type {{ ingest: (ctx: any) => Promise<object[]> }} */\nexport default {\n  async ingest(ctx) {\n    const clientId = ctx?.env?.GMAIL_CLIENT_ID;\n    const clientSecret = ctx?.env?.GMAIL_CLIENT_SECRET;\n    const refreshToken = ctx?.env?.GMAIL_REFRESH_TOKEN;\n    if (!clientId || !clientSecret || !refreshToken) {\n      throw new Error('gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN in .env');\n    }\n\n    const label = ctx?.settings?.label || 'Job Leads';\n    const daysBack = Number(ctx?.settings?.days_back ?? 7);\n    if (!Number.isInteger(daysBack) || daysBack <= 0) {\n      throw new Error(`gmail: invalid days_back \"${ctx?.settings?.days_back}\" (must be a positive integer)`);\n    }\n\n    const token = await getAccessToken({ clientId, clientSecret, refreshToken }, ctx.fetch);\n    const auth = { Authorization: `Bearer ${token}` };\n    const query = `label:\"${label}\" newer_than:${daysBack}d`;\n    ctx.log(`gmail: querying ${query}`);\n\n    // List message ids (paginated). ctx.fetch throws on a non-2xx (with the body\n    // in the message), so a failed page surfaces a clear error.\n    const messages = [];\n    let pageToken = null;\n    do {","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/plugins/gmail/index.mjs#L59-L95","documentation":"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.","triggerScenarios":"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 || ...).","commonSituations":"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).","solutions":["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.","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.","Re-run `node plugins.mjs run gmail`."],"exampleFix":"# before (.env)\nGMAIL_CLIENT_ID=\nGMAIL_CLIENT_SECRET=xxxxx\nGMAIL_REFRESH_TOKEN=yyyyy\n\n# after (.env)\nGMAIL_CLIENT_ID=123456-abc.apps.googleusercontent.com\nGMAIL_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxx\nGMAIL_REFRESH_TOKEN=1//0eXXXXXXXXXXXXXXXX","handlingStrategy":"validation","validationCode":"// Before calling the plugin's ingest, verify the env is populated.\nimport { existsSync, readFileSync } from 'fs';\nfunction gmailEnvReady(env = process.env) {\n  const need = ['GMAIL_CLIENT_ID', 'GMAIL_CLIENT_SECRET', 'GMAIL_REFRESH_TOKEN'];\n  const missing = need.filter((k) => !env[k]);\n  if (missing.length) {\n    return { ready: false, missing };\n  }\n  return { ready: true, missing: [] };\n}\nconst { ready, missing } = gmailEnvReady();\nif (!ready) console.error(`gmail not configured; set in .env: ${missing.join(', ')}`);","typeGuard":"// Narrow plugin ctx.env before passing to ingest.\n/** @typedef {{ GMAIL_CLIENT_ID: string, GMAIL_CLIENT_SECRET: string, GMAIL_REFRESH_TOKEN: string }} GmailEnv */\n/** @param {unknown} e\n *  @returns {e is GmailEnv} */\nfunction isGmailEnv(e) {\n  const env = /** @type {Record<string, unknown>} */ (e ?? {});\n  return typeof env.GMAIL_CLIENT_ID === 'string' && env.GMAIL_CLIENT_ID.length > 0\n    && typeof env.GMAIL_CLIENT_SECRET === 'string' && env.GMAIL_CLIENT_SECRET.length > 0\n    && typeof env.GMAIL_REFRESH_TOKEN === 'string' && env.GMAIL_REFRESH_TOKEN.length > 0;\n}","tryCatchPattern":"try {\n  const jobs = await gmailPlugin.ingest(ctx);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('gmail: missing GMAIL_CLIENT_ID')) {\n    console.warn('Skipping gmail ingest — configure credentials in .env');\n    // non-fatal: continue without gmail leads\n  } else throw err;\n}","preventionTips":["Validate required env vars at startup, before any plugin runs — fail with a clear setup message, not mid-ingest.","Keep a single source of truth for env (.env) and a loader the engine always runs through.","Document the OAuth scope (https://mail.google.com/) the refresh token must cover, so tokens aren't silently scope-limited."],"tags":["gmail","oauth","env","credentials","plugin"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}