nexu-io/open-design · error · DeployError

Cloudflare API token is required.

Error message

Cloudflare API token is required.

What it means

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.

Source

Thrown at apps/daemon/src/deploy.ts:145

export async function writeCloudflarePagesConfig(input: Partial<DeployConfig>) {
  const current = await readCloudflarePagesConfig();
  const tokenInput = typeof input?.token === 'string' ? input.token.trim() : '';
  const cloudflarePages = normalizeCloudflarePagesConfigHints(input?.cloudflarePages, current.cloudflarePages);
  const next: DeployConfig = {
    token:
      tokenInput && tokenInput !== SAVED_CLOUDFLARE_TOKEN_MASK
        ? tokenInput
        : current.token,
    accountId: typeof input?.accountId === 'string' ? input.accountId.trim() : current.accountId,
    // Legacy installs may already have a saved Cloudflare Pages projectName.
    // New writes intentionally stop treating it as user configuration: the
    // deploy route derives a Pages project name from the current OD project,
    // mirroring Vercel's automatic `od-${projectId}` deployment name.
    projectName: '',
  };
  if (Object.keys(cloudflarePages).length > 0) next.cloudflarePages = cloudflarePages;
  if (!next.token) throw new DeployError('Cloudflare API token is required.', 400);
  if (!next.accountId) throw new DeployError('Cloudflare account ID is required.', 400);
  await writeDeployConfigFile(deployConfigPath(CLOUDFLARE_PAGES_PROVIDER_ID), next);
  return publicCloudflarePagesConfig(next);
}

async function writeDeployConfigFile(file: string, config: DeployConfig) {
  await mkdir(path.dirname(file), { recursive: true });
  await writeFile(file, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
  try {
    fs.chmodSync(file, 0o600);
  } catch {
    // Best effort on filesystems that do not support chmod.
  }
}

export function publicDeployConfig(config: Partial<DeployConfig>) {
  return {
    providerId: VERCEL_PROVIDER_ID,

View on GitHub (pinned to 5be4028344)

Solutions

  1. Provide a real Cloudflare API token in the request body (not the mask placeholder).
  2. If resubmitting from a UI that shows the mask, ensure the field is re-entered with the actual token value.
  3. Generate a token at Cloudflare dashboard -> My Profile -> API Tokens with the Pages + DNS permissions the deploy flow needs.
  4. Verify the config file path resolves correctly (OD_USER_STATE_DIR override) and that a prior write actually persisted a token.

Example fix

// before
await writeCloudflarePagesConfig({ token: SAVED_CLOUDFLARE_TOKEN_MASK, accountId: 'abc' }); // first-time save, no prior token

// after
await writeCloudflarePagesConfig({ token: process.env.CF_API_TOKEN, accountId: 'abc' });
Defensive patterns

Strategy: validation

Validate before calling

const token = typeof input?.token === 'string'
  ? input.token.trim()
  : '';
if (!token || token === SAVED_CLOUDFLARE_TOKEN_MASK) {
  const current = await readCloudflarePagesConfig();
  if (!current.token) throw new Error('Provide a real Cloudflare API token');
}

Type guard

function isUnmaskedToken(input: unknown, currentToken: string): boolean {
  return typeof input === 'string'
    && input.trim().length > 0
    && input.trim() !== SAVED_CLOUDFLARE_TOKEN_MASK
    ? true
    : Boolean(currentToken);
}

Try / catch

try {
  await writeCloudflarePagesConfig(input);
} catch (err) {
  if (err instanceof DeployError && err.status === 400 && /token/i.test(err.message)) {
    return res.status(400).json({ error: 'Enter your Cloudflare API token to save credentials.' });
  }
  throw err;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/6c62cb428a21e47f. Report an issue: GitHub.