{"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":"critical","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/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/plugins/notion/_notion.mjs#L49-L85","documentation":"createNotionClient() builds a minimal Notion REST client around an injected fetch function. The token is the single required config field: without a Bearer token every Notion API call would fail with 401 anyway, so the factory throws immediately with a message pointing at NOTION_ACCESS_TOKEN in .env. The function itself never reads process.env — the plugin layer is responsible for loading .env and passing the token in the cfg object.","triggerScenarios":"Calling createNotionClient({ parent }) or createNotionClient({ token: undefined }) when NOTION_ACCESS_TOKEN is absent from the environment/.env, the .env file was not loaded before the plugin initialized, or the config object key is misspelled ({ Token } or { authToken }) so destructuring yields undefined.","commonSituations":"Fresh clone where .env was never created from .env.example; an integration token revoked/deleted in Notion and removed from .env; CI environment where secrets are injected under a different name than the plugin expects; a rename refactor that changed the cfg key but not the call site.","solutions":["Create/edit .env in the project root and add NOTION_ACCESS_TOKEN=secret_... from a Notion internal integration (notion.so/my-integrations).","Verify the .env file is actually loaded before plugin init (e.g. dotenv/config or the engine's env loader runs first) and that the value reaches createNotionClient as cfg.token.","Share the target Notion pages/databases with the integration in Notion (Connections menu) — a valid token without a shared parent will fail later in resolveDBs.","If the token comes from CI secrets, confirm the secret name matches NOTION_ACCESS_TOKEN exactly and is scoped to the job."],"exampleFix":"// before\nconst client = createNotionClient({ parent: process.env.NOTION_PARENT_PAGE_ID });\n\n// after (.env)\n# NOTION_ACCESS_TOKEN=secret_xxxxxxxxxxxx\n\n// code\nconst token = process.env.NOTION_ACCESS_TOKEN;\nif (!token) throw new Error('Set NOTION_ACCESS_TOKEN in .env before using the Notion plugin');\nconst client = createNotionClient({ token, parent: process.env.NOTION_PARENT_PAGE_ID });","handlingStrategy":"validation","validationCode":"// before creating the client\nif (!process.env.NOTION_ACCESS_TOKEN) {\n  throw new Error('NOTION_ACCESS_TOKEN missing: add it to .env (Notion internal integration token)');\n}","typeGuard":"function hasNotionToken(env = process.env): env is typeof env & { NOTION_ACCESS_TOKEN: string } {\n  return typeof env.NOTION_ACCESS_TOKEN === 'string' && env.NOTION_ACCESS_TOKEN.startsWith('secret_');\n}","tryCatchPattern":"let client;\ntry {\n  client = createNotionClient({ token: process.env.NOTION_ACCESS_TOKEN, parent: process.env.NOTION_PARENT_PAGE_ID });\n} catch (e) {\n  if (/NOTION_ACCESS_TOKEN is not set/.test(e.message)) {\n    console.error('Setup incomplete: copy .env.example to .env and fill in NOTION_ACCESS_TOKEN');\n    process.exit(1);\n  }\n  throw e;\n}","preventionTips":["Keep a .env.example with all required Notion vars and fail fast at boot if any are missing.","Never rename cfg keys when refactoring; destructure explicitly so missing keys surface as undefined at one known point.","Rotate tokens in one place (.env) and document where the integration token lives.","Add a startup config checklist (token + parent id + shared pages) to your project README."],"tags":["configuration","environment","authentication","notion"],"backgroundTag":"missing-env-var","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}