{"record":{"id":"6c62cb428a21e47f","repo":"nexu-io/open-design","slug":"cloudflare-api-token-is-required","errorCode":null,"errorMessage":"Cloudflare API token is required.","messagePattern":"Cloudflare API token is required\\.","errorType":"http","errorClass":"DeployError","httpStatus":400,"severity":"error","filePath":"apps/daemon/src/deploy.ts","lineNumber":145,"sourceCode":"\nexport async function writeCloudflarePagesConfig(input: Partial<DeployConfig>) {\n  const current = await readCloudflarePagesConfig();\n  const tokenInput = typeof input?.token === 'string' ? input.token.trim() : '';\n  const cloudflarePages = normalizeCloudflarePagesConfigHints(input?.cloudflarePages, current.cloudflarePages);\n  const next: DeployConfig = {\n    token:\n      tokenInput && tokenInput !== SAVED_CLOUDFLARE_TOKEN_MASK\n        ? tokenInput\n        : current.token,\n    accountId: typeof input?.accountId === 'string' ? input.accountId.trim() : current.accountId,\n    // Legacy installs may already have a saved Cloudflare Pages projectName.\n    // New writes intentionally stop treating it as user configuration: the\n    // deploy route derives a Pages project name from the current OD project,\n    // mirroring Vercel's automatic `od-${projectId}` deployment name.\n    projectName: '',\n  };\n  if (Object.keys(cloudflarePages).length > 0) next.cloudflarePages = cloudflarePages;\n  if (!next.token) throw new DeployError('Cloudflare API token is required.', 400);\n  if (!next.accountId) throw new DeployError('Cloudflare account ID is required.', 400);\n  await writeDeployConfigFile(deployConfigPath(CLOUDFLARE_PAGES_PROVIDER_ID), next);\n  return publicCloudflarePagesConfig(next);\n}\n\nasync function writeDeployConfigFile(file: string, config: DeployConfig) {\n  await mkdir(path.dirname(file), { recursive: true });\n  await writeFile(file, `${JSON.stringify(config, null, 2)}\\n`, { mode: 0o600 });\n  try {\n    fs.chmodSync(file, 0o600);\n  } catch {\n    // Best effort on filesystems that do not support chmod.\n  }\n}\n\nexport function publicDeployConfig(config: Partial<DeployConfig>) {\n  return {\n    providerId: VERCEL_PROVIDER_ID,","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/deploy.ts#L127-L163","documentation":"Thrown by writeCloudflarePagesConfig in apps/daemon/src/deploy.ts:145 as a `DeployError('Cloudflare API token is required.', 400)`. After merging the submitted input into `next`, if `next.token` is falsy the save is aborted. Note the token merge logic: if the submitted token equals `SAVED_CLOUDFLARE_TOKEN_MASK` (the masked placeholder shown back to the UI), the previously saved `current.token` is reused; so this throws when the user submits the mask (or empty) AND no prior token was ever saved.","triggerScenarios":"POSTing Cloudflare config (token/accountId) to the deploy config route where the trimmed submitted token is empty or is the mask string, and `current.token` from the existing cloudflare-pages.json is also empty (file missing or never written).","commonSituations":"First-time Cloudflare setup where the user left the token field blank; the masked placeholder was submitted unchanged after a fresh install; the cloudflare-pages.json under OD_USER_STATE_DIR (~/.open-design) was deleted so `readCloudflarePagesConfig` returned an empty token; user copy-pasted only the accountId.","solutions":["Provide a real Cloudflare API token in the request body (not the mask placeholder).","If resubmitting from a UI that shows the mask, ensure the field is re-entered with the actual token value.","Generate a token at Cloudflare dashboard -> My Profile -> API Tokens with the Pages + DNS permissions the deploy flow needs.","Verify the config file path resolves correctly (OD_USER_STATE_DIR override) and that a prior write actually persisted a token."],"exampleFix":"// before\nawait writeCloudflarePagesConfig({ token: SAVED_CLOUDFLARE_TOKEN_MASK, accountId: 'abc' }); // first-time save, no prior token\n\n// after\nawait writeCloudflarePagesConfig({ token: process.env.CF_API_TOKEN, accountId: 'abc' });","handlingStrategy":"validation","validationCode":"const token = typeof input?.token === 'string'\n  ? input.token.trim()\n  : '';\nif (!token || token === SAVED_CLOUDFLARE_TOKEN_MASK) {\n  const current = await readCloudflarePagesConfig();\n  if (!current.token) throw new Error('Provide a real Cloudflare API token');\n}","typeGuard":"function isUnmaskedToken(input: unknown, currentToken: string): boolean {\n  return typeof input === 'string'\n    && input.trim().length > 0\n    && input.trim() !== SAVED_CLOUDFLARE_TOKEN_MASK\n    ? true\n    : Boolean(currentToken);\n}","tryCatchPattern":"try {\n  await writeCloudflarePagesConfig(input);\n} catch (err) {\n  if (err instanceof DeployError && err.status === 400 && /token/i.test(err.message)) {\n    return res.status(400).json({ error: 'Enter your Cloudflare API token to save credentials.' });\n  }\n  throw err;\n}","preventionTips":["Never submit the mask placeholder on a first-time save; always send the real token.","Gate the Save button on a non-empty, non-mask token field when no prior token exists.","Generate the token with Cloudflare Pages + DNS edit permissions to avoid follow-up errors."],"tags":["deploy","cloudflare","validation","config","credentials"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}