# Overview

> What vwaffle exposes, who should run it, the vwaffle binary entry point, and the shortest pull / plan / apply path.

- 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

- `package.json`
- `src/index.ts`
- `src/api.ts`
- `README.md`

---

---
title: "Overview"
description: "What vwaffle exposes, who should run it, the vwaffle binary entry point, and the shortest pull / plan / apply path."
---

`vwaffle` is a Node CLI that treats a local `firewall.config.json` as the desired Vercel Firewall config. The published binary is `dist/index.js` (`bin.vwaffle`); source entry is `src/index.ts` (`#!/usr/bin/env node`). Commands read or write `GET`/`PUT /v1/security/firewall/config` after resolving `VERCEL_TOKEN`, project ID, and optional team ID.

<Note>
`init` writes a starter file and does not call Vercel. `pull`, `plan`, and `apply` require `VERCEL_TOKEN` plus a project ID. `apply` without `--yes` or `--dry-run` throws.
</Note>

## Who runs it

Use `vwaffle` when the Vercel Firewall should live in git and change through a reviewed file rather than dashboard-only edits.

| Operator | Typical command |
| --- | --- |
| Author a first file | `vwaffle init` or `vwaffle pull --output firewall.config.json` |
| Preview a local edit | `vwaffle plan` |
| Fail CI on dashboard drift | `vwaffle plan --check` |
| Push the file as source of truth | `vwaffle apply --yes` |
| Inspect the interpolated PUT body | `vwaffle apply --dry-run` |

Package metadata: `name` `vwaffle`, `version` `0.1.0`, `engines.node` `>=18`, license Apache-2.0. Published tarball contents are `dist`, `README.md`, and `LICENSE`.

## Binary and commands

`parseArgs(process.argv.slice(2))` accepts one command and the flags below. Unknown commands and flags fail with `process.exitCode = 1`.

| Command | API | Default file | Exit / guard |
| --- | --- | --- | --- |
| `init` | none | writes `--config` (default `firewall.config.json`) | refuses to overwrite an existing path |
| `pull` | `GET …/active` | stdout, or `--output FILE` | needs token + project |
| `plan` | `GET …/active` | reads `--config` | `--check` sets `exitCode = 1` when the diff is not `No drift detected.` |
| `apply` | `GET …/active`, then `PUT …` | reads `--config` | requires `--yes` or `--dry-run`; no PUT when there is no drift |
| `help` / `-h` / `--help` | none | — | prints usage |
| `version` / `-v` / `--version` | none | — | prints `0.1.0` |

<ParamField body="--config" type="string" required={false}>
Path to the desired JSON. Short flag `-c`. Default `firewall.config.json`, resolved from `process.cwd()`.
</ParamField>

<ParamField body="--output" type="string" required={false}>
`pull` only. Short flag `-o`. Write the live config instead of printing it.
</ParamField>

<ParamField body="--project" type="string" required={false}>
Vercel project ID. Overrides `VERCEL_PROJECT_ID` and `.vercel/project.json` `projectId`.
</ParamField>

<ParamField body="--team" type="string" required={false}>
Vercel team ID. Overrides `VERCEL_TEAM_ID` and `.vercel/project.json` `orgId`.
</ParamField>

<ParamField body="--check" type="boolean" required={false}>
`plan` only. Non-zero exit when live JSON differs from interpolated desired JSON.
</ParamField>

<ParamField body="--dry-run" type="boolean" required={false}>
`apply` only. Print the redacted desired payload and return. No `resolveContext`, no GET, no PUT.
</ParamField>

<ParamField body="--yes" type="boolean" required={false}>
`apply` only. Required to PUT unless `--dry-run` is set.
</ParamField>

## Runtime layout

:::files
package.json          bin.vwaffle → ./dist/index.js; engines.node >=18
src/index.ts          parseArgs, init / pull / plan / apply, help, version
src/api.ts            resolveContext, getActiveConfig, putConfig
src/config.ts         loadDesiredConfig, interpolate, redacted
src/diff.ts           diff(live, desired); NO_DRIFT
src/types.ts          FirewallConfig, CliOptions, ResolvedContext
dist/index.js         published Node target (bun build --target node)
:::

```mermaid
flowchart LR
  subgraph cwd [Working directory]
    CFG["firewall.config.json"]
    LINK[".vercel/project.json"]
  end
  subgraph bin ["vwaffle → dist/index.js"]
    IDX["src/index.ts"]
    CFGLOAD["src/config.ts"]
    DIFF["src/diff.ts"]
    API["src/api.ts"]
  end
  subgraph vercel ["VERCEL_API_URL or api.vercel.com"]
    GET["GET /v1/security/firewall/config/active"]
    PUT["PUT /v1/security/firewall/config"]
  end
  CFG --> CFGLOAD
  LINK --> API
  IDX --> CFGLOAD
  IDX --> DIFF
  IDX --> API
  API --> GET
  API --> PUT
```

`resolveContext` requires `VERCEL_TOKEN`. Project ID is `--project`, then `VERCEL_PROJECT_ID`, then `.vercel/project.json` `projectId`. Team ID is `--team`, then `VERCEL_TEAM_ID`, then `orgId` from the same file; team is optional and omitted from the query string when unset. The API base is `${VERCEL_API_URL ?? 'https://api.vercel.com'}/v1/security/firewall/config`. Requests send `Authorization: Bearer <token>`. `getActiveConfig` unwraps a `{ active }` envelope when present.

## Desired file

The desired file is the JSON body sent on PUT. `FirewallConfig` fields used by the starter and types:

| Key | Role |
| --- | --- |
| `firewallEnabled` | enable/disable the firewall |
| `managedRules` | managed sets such as `owasp: { active }` |
| `rules` | custom rules (`name`, `active`, `conditionGroup`, `action.mitigate`) |
| `ips` | IP entries (`ip`, `hostname`, `action`, optional `notes`) |
| `crs` | optional CRS managed-rule map |

String values may contain `${VAR_NAME}` (`[A-Za-z_][A-Za-z0-9_]*`). `interpolate` expands them from the environment and records values for redaction as `[REDACTED]`.

| Command | `loadDesiredConfig` `strict` | Missing `${VAR}` |
| --- | --- | --- |
| `plan` | `false` | warn, drop matching `rules` entries, continue |
| `apply --dry-run` | `false` | same as `plan` |
| `apply --yes` | `true` | throw `missing environment variables: …` |

## Shortest pull / plan / apply path

<Steps>
<Step title="Set a token and project">
Export `VERCEL_TOKEN`. Supply a project with `--project`, `VERCEL_PROJECT_ID`, or a prior `vercel link` (`.vercel/project.json`). Add `--team` / `VERCEL_TEAM_ID` when the token is team-scoped.
</Step>
<Step title="Create or fetch the file">
`vwaffle init` writes the bundled starter (OWASP managed rule off, one `/.git` deny rule, empty `ips`) and refuses to overwrite. `vwaffle pull --output firewall.config.json` writes the live active config instead.
</Step>
<Step title="Preview">
`vwaffle plan` diffs live JSON against interpolated desired JSON. Identical configs print `No drift detected.` Drift prints a tab-indented unified-style patch headed `--- live firewall configuration` / `+++ desired firewall configuration`.
</Step>
<Step title="Apply">
`vwaffle apply --yes` reprints the redacted diff, then PUTs unless the result is `No drift detected.` Success prints `Applied firewall configuration` and appends `(version N)` when the API returns `version`.
</Step>
</Steps>

<RequestExample>
```sh
export VERCEL_TOKEN=...
vwaffle pull --output firewall.config.json
vwaffle plan
vwaffle apply --yes
```
</RequestExample>

<ResponseExample>
```text
No drift detected.
```
</ResponseExample>

<Warning>
`apply` without `--yes` or `--dry-run` throws: `apply requires --yes. Use --dry-run to inspect the payload without calling Vercel.`
</Warning>

## Failure signals

| Condition | Message / exit |
| --- | --- |
| Missing token | `VERCEL_TOKEN is required. …` |
| Missing project | `A project is required. Pass --project, set VERCEL_PROJECT_ID, or run \`vercel link\` …` |
| Existing init target | `<config> already exists; refusing to overwrite.` |
| Unset vars on apply | `missing environment variables: NAME ($.path), …` |
| HTTP error | `Vercel API <status> <statusText>: <body>` |
| `plan --check` drift | printed diff; `process.exitCode = 1` |
| Unknown command / flag | `unknown command …` / `unknown option …`; `exitCode = 1` |

Install and invoke the published binary on the [Installation](/installation) page. Walk the first successful apply on [Quickstart](/quickstart).

## Next

<CardGroup>
<Card title="Installation" href="/installation">
Node >=18, global npm install, npx/bunx, and `dist/index.js`.
</Card>
<Card title="Quickstart" href="/quickstart">
Token, scaffold or pull, plan, apply --yes, first success signal.
</Card>
<Card title="Project and team context" href="/project-context">
How resolveContext picks token, project ID, and team ID.
</Card>
<Card title="Plan and apply lifecycle" href="/plan-apply-lifecycle">
--check, --dry-run, --yes, and when PUT runs.
</Card>
<Card title="CLI reference" href="/cli-reference">
Commands, flags, and help text as implemented.
</Card>
</CardGroup>
