{"record":{"id":"48bf0f7f469dca0e","repo":"santifer/career-ops","slug":"notion-access-token-is-not-set-env-the-notion","errorCode":null,"errorMessage":"NOTION_ACCESS_TOKEN is not set (.env) — the Notion plugin needs it to read/write.","messagePattern":"NOTION_ACCESS_TOKEN is not set \\(\\.env\\) — the Notion plugin needs it to read/write\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/notion/_notion.mjs","lineNumber":67,"sourceCode":"  const str = String(text ?? '');\n  const out = [];\n  for (let i = 0; i < str.length || out.length === 0; i += MAX) out.push({ type: 'text', text: { content: str.slice(i, i + MAX) } });\n  return out;\n}\n\nexport function plain(prop) {\n  return (prop?.title || prop?.rich_text || []).map((t) => t.plain_text).join('');\n}\n\n/**\n * Build a Notion client bound to one user's token + parent page. Network goes\n * through the injected `fetchFn` (the plugin passes ctx.fetch so the engine's\n * allowedHosts/HTTPS/redirect guard applies); falls back to global fetch for\n * standalone use. Nothing here reads process.env.\n * @param {{ token: string, parent: string, fetch?: Function }} cfg\n */\nexport function createNotionClient({ token, parent, fetch: fetchFn = globalThis.fetch }) {\n  if (!token) throw new Error('NOTION_ACCESS_TOKEN is not set (.env) — the Notion plugin needs it to read/write.');\n  const HEADERS = { Authorization: `Bearer ${token}`, 'Notion-Version': '2025-09-03', 'Content-Type': 'application/json' };\n  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));\n\n  async function api(path, method, body) {\n    await sleep(360); // ~3 req/s\n    // ctx.fetch throws on non-2xx (its message carries the body); the !r.ok\n    // branch below is the fallback when a plain global fetch is injected.\n    const r = await fetchFn(`https://api.notion.com/v1/${path}`, { method, headers: HEADERS, body: body ? JSON.stringify(body) : undefined });\n    const j = await r.json();\n    if (!r.ok) throw new Error(`Notion ${method} ${path} -> ${j.code}: ${j.message}`);\n    return j;\n  }\n\n  /** Create a page in a data source. `markdown` (optional) becomes the page body. */\n  async function createPage(dataSourceId, properties, markdown) {\n    const body = { parent: { type: 'data_source_id', data_source_id: dataSourceId }, properties };\n    if (markdown) body.markdown = markdown;\n    return api('pages', 'POST', body);","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/plugins/notion/_notion.mjs#L49-L85","documentation":"The Notion plugin helper createNotionClient() builds a scoped API client bound to one user's integration token. A token is mandatory because every Notion API call is authenticated with `Bearer <token>`. This guard throws before any network call when the token argument is falsy, so it fails fast rather than emitting 401s downstream.","triggerScenarios":"Calling createNotionClient({token, ...}) — directly or via plugins/notion/index.mjs's clientFromCtx() — when NOTION_ACCESS_TOKEN is absent from .env (ctx.env.NOTION_ACCESS_TOKEN is undefined).","commonSituations":"Notion integration not created yet in Notion (Settings → My connections → Develop your own integration); integration created but its secret never copied into .env; .env var name typoed (e.g. NOTION_TOKEN instead of NOTION_ACCESS_TOKEN).","solutions":["Create an internal integration at https://www.notion.so/profile/integrations and copy the Internal Integration Secret.","Add `NOTION_ACCESS_TOKEN=<secret>` to .env (the token starts with `ntn_` or `secret_`).","Share the target Notion page(s) with the integration (Page menu → Connect to → your integration).","Re-run `node plugins.mjs run notion`."],"exampleFix":"# before (.env)\n# (no Notion vars)\n\n# after (.env)\nNOTION_ACCESS_TOKEN=ntn_xxxxxxxxxxxxxxxxxxxxxxxxxxxx\nNOTION_PARENT_PAGE_ID=abc123def456","handlingStrategy":"validation","validationCode":"// Guard the token before constructing the client.\nfunction notionClientFromCtx(ctx) {\n  const token = ctx?.env?.NOTION_ACCESS_TOKEN;\n  if (!token) {\n    throw new Error('Configure NOTION_ACCESS_TOKEN in .env before using the Notion plugin.');\n  }\n  return createNotionClient({ token, parent: ctx?.env?.NOTION_PARENT_PAGE_ID, fetch: ctx.fetch });\n}","typeGuard":"/** @param {unknown} t @returns {t is string} */\nfunction isNonEmptyToken(t) {\n  return typeof t === 'string' && t.trim().length > 0;\n}","tryCatchPattern":"try {\n  const client = createNotionClient({ token, parent });\n} catch (err) {\n  if (err instanceof Error && err.message.includes('NOTION_ACCESS_TOKEN')) {\n    console.error('Notion disabled — set NOTION_ACCESS_TOKEN in .env.');\n  } else throw err;\n}","preventionTips":["Centralize a `requiredEnv(['NOTION_ACCESS_TOKEN', 'NOTION_PARENT_PAGE_ID'])` helper that fails at startup with a clear message.","Treat an absent token as a disabled plugin (skip gracefully) rather than throwing mid-operation."],"tags":["notion","env","credentials","plugin","auth"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}