# Authentication and context errors

> Missing VERCEL_TOKEN, unresolved project or team IDs, and failed Firewall API requests from resolveContext and request.

- Repository: jaredpalmer/vwaffle
- GitHub: https://github.com/jaredpalmer/vwaffle
- Human docs: https://grok-wiki.com/public/docs/jaredpalmer-vwaffle-7983cb893581
- Complete Markdown: https://grok-wiki.com/public/docs/jaredpalmer-vwaffle-7983cb893581/llms-full.txt

## Source Files

- `src/api.ts`
- `src/index.ts`
- `src/types.ts`
- `README.md`

---

---
title: "Authentication and context errors"
description: "Missing VERCEL_TOKEN, unresolved project or team IDs, and failed Firewall API requests from resolveContext and request."
---

`resolveContext` and `request` in `src/api.ts` are the only auth and Firewall HTTP surfaces. `pull`, `plan`, and `apply` (without `--dry-run`) call `resolveContext` first. A missing `VERCEL_TOKEN` or project ID throws locally. A non-OK Vercel response throws after `fetch`. Every thrown error is printed as `vwaffle: <message>` and sets `process.exitCode` to `1`.

## Commands that hit auth

| Command | Calls `resolveContext` | Calls `getActiveConfig` | Calls `putConfig` |
| --- | --- | --- | --- |
| `vwaffle pull` | Yes | Yes (`GET /active`) | No |
| `vwaffle plan` | Yes | Yes | No |
| `vwaffle apply --yes` | Yes | Yes | Yes, unless the live/desired diff is `No drift.` |
| `vwaffle apply --dry-run` | No | No | No |
| `vwaffle init`, `help`, `--version` | No | No | No |

`apply` without `--yes` or `--dry-run` throws `apply requires --yes. Use --dry-run to inspect the payload without calling Vercel.` before any token or project check.

## How errors are printed

```ts
main().catch((error: unknown) => {
	console.error(`vwaffle: ${error instanceof Error ? error.message : String(error)}`);
	process.exitCode = 1;
});
```

There is no retry, no status-specific mapping, and no stack dump. `src/config.test.ts` does not cover `resolveContext` or `request`.

<ResponseExample>

```text
vwaffle: VERCEL_TOKEN is required. Create a Vercel API token (https://vercel.com/account/tokens) and export it before running this command.
```

</ResponseExample>

## Resolution order

`resolveContext(overrides)` builds a `ResolvedContext` of `{ token, projectId, teamId? }`. First match wins. Empty string is falsy for token and project and is treated as unset.

| Field | Sources | Required |
| --- | --- | --- |
| `token` | `VERCEL_TOKEN` only | Yes |
| `projectId` | `--project`, then `VERCEL_PROJECT_ID`, then `.vercel/project.json` `projectId` | Yes |
| `teamId` | `--team`, then `VERCEL_TEAM_ID`, then `.vercel/project.json` `orgId` | No |

`--project` and `--team` are parsed in `src/index.ts` and passed through as `overrides.project` / `overrides.team`. There is no token flag.

`.vercel/project.json` is read from `process.cwd()` via `readJson`. Missing file, unreadable file, or invalid JSON is swallowed; a linked project is optional when flags or env vars supply the IDs.

```json
{
	"projectId": "prj_...",
	"orgId": "team_..."
}
```

`orgId` is the team source. A `teamId` key in that file is ignored.

```mermaid
flowchart TD
  subgraph resolveContext["resolveContext"]
    T["VERCEL_TOKEN"] -->|missing or empty| E1["throw token required"]
    T -->|present| L["read .vercel/project.json"]
    L --> P["--project / VERCEL_PROJECT_ID / projectId"]
    P -->|missing or empty| E2["throw project required"]
    P -->|present| TM["--team / VERCEL_TEAM_ID / orgId"]
    TM --> CTX["ResolvedContext"]
  end
  subgraph requestFn["request"]
    CTX --> URL["GET /active or PUT ''"]
    URL -->|response.ok| OK["return parsed body"]
    URL -->|not ok| E3["throw Vercel API status + body"]
  end
```

## Missing `VERCEL_TOKEN`

Thrown when `process.env.VERCEL_TOKEN` is missing or empty. Checked before `.vercel/project.json` is read.

<Warning>
`VERCEL_TOKEN is required. Create a Vercel API token (https://vercel.com/account/tokens) and export it before running this command.`
</Warning>

<ParamField body="VERCEL_TOKEN" type="string" required>
Bearer token sent as `Authorization: Bearer ${context.token}`. Not read from `.vercel/project.json` or a CLI flag.
</ParamField>

<Steps>
<Step title="Create and export a token">

Create a token at `https://vercel.com/account/tokens` and export it in the same shell (or CI job) that runs `vwaffle`.

```sh
export VERCEL_TOKEN=...
```

</Step>
<Step title="Re-run a command that calls the API">

```sh
vwaffle pull
# or
vwaffle plan
```

</Step>
</Steps>

## Unresolved project ID

Thrown after the token check when `--project`, `VERCEL_PROJECT_ID`, and `linked.projectId` are all missing or empty.

<Warning>
`A project is required. Pass --project, set VERCEL_PROJECT_ID, or run \`vercel link\` so .vercel/project.json exists.`
</Warning>

<ParamField body="--project" type="string">
CLI override. Highest priority. `vwaffle --project ID` requires a value or throws `--project requires a value`.
</ParamField>

<ParamField body="VERCEL_PROJECT_ID" type="string">
Used when `--project` is omitted.
</ParamField>

<Tabs>
<Tab title="Flag">

```sh
vwaffle pull --project prj_...
```

</Tab>
<Tab title="Environment">

```sh
export VERCEL_PROJECT_ID=prj_...
vwaffle pull
```

</Tab>
<Tab title="vercel link">

```sh
vercel link
# writes .vercel/project.json with projectId (and orgId)
vwaffle pull
```

</Tab>
</Tabs>

<Note>
A present `.vercel/project.json` that lacks `projectId` does not satisfy the check. `vercel link` must be run from the same working directory `vwaffle` uses.
</Note>

## Team ID is optional in `resolveContext`

`teamId` is never required by `resolveContext`. Missing team does not throw. `requestUrl` adds `teamId` only when the resolved value is truthy.

An empty `--team` or `VERCEL_TEAM_ID=""` is stored on `ResolvedContext` but omitted from the query string (`if (context.teamId)`).

<Tip>
A team-scoped token or team project often still needs `teamId` on the request. That failure arrives later as a `Vercel API <status> ...` error, not as a local “team required” message.
</Tip>

<ParamField body="--team" type="string">
CLI override. Highest priority for team scope.
</ParamField>

<ParamField body="VERCEL_TEAM_ID" type="string">
Used when `--team` is omitted.
</ParamField>

```sh
vwaffle pull --project prj_... --team team_...
```

## Failed Firewall API requests

After context resolves, `getActiveConfig` and `putConfig` call `request`. Base URL:

```text
${VERCEL_API_URL ?? 'https://api.vercel.com'}/v1/security/firewall/config
```

`VERCEL_API_URL` is honored in `src/api.ts` and is not listed in `vwaffle help`.

:::endpoint GET /v1/security/firewall/config/active
Fetch the live firewall config (`getActiveConfig`).

Query: `projectId` (always), `teamId` (when truthy).

Headers: `Authorization: Bearer <VERCEL_TOKEN>`, `Content-Type: application/json`.
:::

:::endpoint PUT /v1/security/firewall/config
Write the desired config (`putConfig`). Same query params and headers. Body is the interpolated `FirewallConfig` JSON.
:::

On every response, `request` reads `response.text()`, parses JSON when possible, and throws when `!response.ok`:

```text
Vercel API ${status} ${statusText}: ${body}
```

| Body shape | Value interpolated into the message |
| --- | --- |
| Empty | `null` (`JSON.stringify(null)`) |
| Valid JSON object/array | `JSON.stringify(data)` |
| Non-JSON text | Raw response text |

The CLI does not branch on 401, 403, 404, or 429. Treat the status and body from Vercel as the diagnostic.

<AccordionGroup>
<Accordion title="401 / 403 after context resolved">
Token is present but rejected, or the token lacks Firewall / project / team scope. Confirm `VERCEL_TOKEN` is the token you intended, then pass `--team` / `VERCEL_TEAM_ID` / `orgId` if the project is on a team.
</Accordion>
<Accordion title="404 or project-not-found body">
`projectId` resolved to a value Vercel does not accept for this token. Check `--project` / `VERCEL_PROJECT_ID` / `.vercel/project.json` `projectId` against the Vercel dashboard. A personal-account project ID with a team token (or the reverse) typically fails here, not in `resolveContext`.
</Accordion>
<Accordion title="Non-OK on apply PUT only">
`apply --yes` always `GET`s `/active` first. A GET that succeeds and a PUT that fails means context is valid and the payload or API rejected the write. Inspect `apply --dry-run` for the redacted body; that path never calls `request`.
</Accordion>
<Accordion title="Network or parse failures">
`fetch` rejections (DNS, TLS, offline) surface as the raw error message under the same `vwaffle:` prefix. Invalid JSON on an OK response is returned as-is by `request` and is not remapped.
</Accordion>
</AccordionGroup>

## Failure order on `apply --yes`

Local checks run before HTTP.

| Order | Check | Typical message |
| --- | --- | --- |
| 1 | `--yes` or `--dry-run` | `apply requires --yes. ...` |
| 2 | `loadDesiredConfig({ strict: true })` | `missing environment variables: ...` |
| 3 | `resolveContext` token | `VERCEL_TOKEN is required. ...` |
| 4 | `resolveContext` project | `A project is required. ...` |
| 5 | `GET /active` | `Vercel API <status> <statusText>: ...` |
| 6 | `PUT` (skipped when diff is `No drift.`) | `Vercel API <status> <statusText>: ...` |

`plan` loads the desired file with `strict: false` (warnings only), then follows steps 3–5. Unset `${VAR_NAME}` interpolation is a separate failure path.

## Verify a working context

<Steps>
<Step title="Export token and IDs in the same process">

```sh
export VERCEL_TOKEN=...
export VERCEL_PROJECT_ID=prj_...
export VERCEL_TEAM_ID=team_...   # if the project is on a team
```

</Step>
<Step title="Confirm pull can GET /active">

```sh
vwaffle pull
```

Success prints the live JSON (or writes `--output`). Failure is either a local `resolveContext` string or `Vercel API ...`.

</Step>
<Step title="Confirm plan can reuse the same context">

```sh
vwaffle plan
```

A printed diff or `No drift.` means auth and project resolution succeeded.

</Step>
</Steps>

<Check>
CI jobs must inject `VERCEL_TOKEN` and usually `VERCEL_PROJECT_ID` / `VERCEL_TEAM_ID`. `.vercel/project.json` is typically not present in a clean checkout unless `vercel link` ran in that workspace.
</Check>

## Related pages

<CardGroup>
<Card title="Project and team context" href="/project-context">
How `resolveContext` selects token, project, and team from flags, env, and `.vercel/project.json`.
</Card>
<Card title="Firewall API client" href="/firewall-api-client">
`getActiveConfig`, `putConfig`, `request`, and `requestUrl`.
</Card>
<Card title="Environment variables" href="/environment-variables">
`VERCEL_TOKEN`, `VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, and interpolated `${VAR_NAME}` values.
</Card>
<Card title="Interpolation and check failures" href="/interpolation-and-check-failures">
Unset `${VAR_NAME}` on plan versus apply, and `plan --check` exit 1.
</Card>
<Card title="Apply from CI" href="/apply-from-ci">
Token, project, and team inputs for `apply --yes` on merge.
</Card>
<Card title="CLI reference" href="/cli-reference">
`--project`, `--team`, and the commands that call the API.
</Card>
</CardGroup>
