> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alfiz.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# alfiz-verify CLI: Static Permission Verification

> alfiz-verify checks typed permission keys, gate coverage, gate shapes, and catalog conventions. Run in CI to make ungated server actions a build failure.

`alfiz-verify` is a static analysis tool that enforces Alfiz's four-point wiring checklist at build time. It parses your TypeScript source files (no type-checker invocation needed) and scans for permission check and gate call sites, coverage gaps, and catalog convention violations. Running it in CI means a new server action with no gate, a typo'd permission key, or a visibility affordance used as a gate becomes a build failure, not a production bug.

## Installation

```bash theme={null}
npm install --save-dev @alfiz/verify
```

## Usage

```bash theme={null}
alfiz-verify [--config <path>]
```

By default, `alfiz-verify` reads `alfiz-verify.config.json` in the current working directory. Pass `--config <path>` to use a different location.

## The `codegen` subcommand

```bash theme={null}
alfiz-verify codegen --catalog <document.json> [--out <file>] [--prefix <Name>]
```

Emits derived key, pattern, and scope-id type unions from a published catalog document, as a dependency-free `.ts` module. Without `--out` it writes to stdout. `--prefix` names the emitted aliases (`<Prefix>Key`, `<Prefix>Pattern`, `<Prefix>ScopeType`, `<Prefix>ScopeId`); it defaults to `Alfiz`.

`defineCatalog` derives those unions from your catalog *literal*, so code importing the source module already has them. This is for everything that consumes the published **document** instead. A document is data rather than a literal, so its keys reach the type system only through codegen:

```bash theme={null}
# Your own catalog, consumed from another repo or a generated client
alfiz-verify codegen --catalog alfiz-catalog.json --out alfiz.gen.ts
```

```ts theme={null}
import { catalogFromDocument } from "@alfiz/core";
import type { AlfizKey, AlfizPattern, AlfizScopeId } from "./alfiz.gen.js";

const catalog = catalogFromDocument<AlfizKey, AlfizPattern, AlfizScopeId>(doc);
```

It is also how a wildcard [import](/catalog/imports) closes its key union, so generate from the namespace owner's document, then pin it with `importedKeys<ZoomKey>({ ... })`. Members are sorted, so regenerating diffs exactly the catalog change. A malformed document fails here, loudly, rather than emitting types from garbage.

### The programmatic form

When you generate inside a build script instead of a shell step, call `generateCatalogTypes` directly. It returns the module source as a string, and writing it is yours to do.

```ts theme={null}
import { generateCatalogTypes } from "@alfiz/verify";
import { writeFileSync } from "node:fs";
import { catalog } from "./src/alfiz.js";

const source = generateCatalogTypes(catalog.toDocument(), {
  prefix: "Zoom",                                  // default "Alfiz"
  banner: ["Generated from registry:zoom@3.1.0."], // extra header lines
});

writeFileSync("zoom.gen.ts", source);
```

## What it checks

`alfiz-verify` runs six categories of checks on every scan. Every finding carries a rule name, the same name you use to suppress it per line, and the same name the `VerifyIssue` shape reports:

### 1. Typed key validation

Every string literal passed to a gate function (`can`, `require`, `gateAction`, etc.) that belongs to a known namespace is validated against the catalog. Unknown keys produce errors; near-misses (group paths where patterns are required) produce a specific suggestion:

```
✖ [unknown-pattern] src/actions/docs.ts:12 — "docs.files" is a group, not a key — groups are folders, and subtree patterns end in .*: did you mean "docs.files.*"?
✖ [unknown-pattern] src/actions/docs.ts:34 — "docs.files.publsh" is not in the catalog (typo, or an undeclared key)
```

### 2. Coverage linting

Warnings on catalog leaves that no gate or nav item references. Errors on exported async functions in server files that contain no gate call at all:

```
⚠ [unreferenced-leaf] <catalog> — "docs.admin.manage_billing" is declared but referenced by no gate or nav item — dead permission, or missing enforcement?
✖ [ungated-action] src/actions/billing.ts:8 — exported server action "updatePaymentMethod" contains no gate — every action gates on a concrete permission before doing work
```

<Note>
  `alfiz_internal.*` keys are exempt from coverage warnings, because they are gated inside Alfiz's own admin surfaces, not your application code.
</Note>

### 3. Gate-shape linting

Errors when a visibility affordance (`canAny`, `requireAny`, or `holds`) is used directly as a gate in a server file. Visibility affordances tell you whether any matching key is held *somewhere*; they are not access control decisions. (`heldKeys` is a property access rather than a call, so it cannot read as a gate.)

```
✖ [visibility-as-gate] src/actions/export.ts:15 — canAny() is a visibility affordance, never a gate — server actions and route handlers gate on a concrete permission (can/require*)
```

### 4. Catalog linting

Convention violations detected by running `lintCatalog` on the loaded catalog document:

```
✖ [catalog] docs.files — below the naming floor: every tab defines at least one read permission (`read` or `read_<thing>`)
⚠ [catalog] docs.admin.ManageBilling — actions are named `<verb>_<noun>` in snake_case (destructive actions may stand alone, e.g. `delete`)
✖ [catalog] docs.doc — requestable without a resolvable policy: declare at least one approval stage
```

### 5. Import validation

Permissions from a namespace your catalog neither owns nor [imports](/catalog/imports) are reported as **implicit imports**: an error when no import source is configured, a warning when one is. A reach beyond what an import *does* cover is always an error, with a did-you-mean drawn from the namespace owner's published document:

```
✖ [unknown-import] src/actions/meet.ts:8 — "zoom.hostt" is not in the catalog (typo, or an undeclared key) — did you mean "zoom.host"?
⚠ [implicit-import] src/actions/pay.ts:11 — "stripe.charges.read" is in namespace "stripe", which this catalog neither owns nor imports — declare it: imports: { stripe: { permissions: { "stripe.charges.read": true } } }
```

Configure the severity with [`importSource` and `implicitImports`](/api/verify-config), and point the CLI at your import manifest so it knows what you legitimately reference. Verification is offline and deterministic: it never fetches a catalog to grade your code.

### 6. The client-reach guard

Errors when an identifier you have named in `forbidClientIdentifiers` appears in a `"use client"` module. This is the guard that keeps service-key material out of the bundle:

```
✖ [client-reachable-secret] src/components/Admin.tsx:4 — "ALFIZ_SERVICE_KEYS" referenced in a "use client" module — service-key material must never be client-reachable
```

## Rule inventory

| Rule                      | Severity                                | Fires on                                                                         |
| ------------------------- | --------------------------------------- | -------------------------------------------------------------------------------- |
| `unknown-pattern`         | Error                                   | A key or pattern in a namespace you own that the catalog does not declare        |
| `unknown-import`          | Error                                   | A reach beyond what an import actually covers                                    |
| `implicit-import`         | Error, or warning with an import source | A permission in a namespace you neither own nor import                           |
| `visibility-as-gate`      | Error                                   | `canAny`, `requireAny`, or `holds` used as a gate in a server file               |
| `missing-condition`       | Error                                   | A gate for a `requiresCondition` key with no visible `condition` in its options  |
| `ungated-action`          | Error                                   | An exported server action containing no gate at all                              |
| `client-reachable-secret` | Error                                   | A forbidden identifier in a `"use client"` module                                |
| `unreferenced-leaf`       | Warning                                 | A catalog leaf no gate or nav item references                                    |
| `ignored-file`            | Warning                                 | An `alfiz-verify-ignore-file` pragma with no reason, or placed where it is inert |
| `stale-suppression`       | Warning                                 | A per-line pragma with no rule, no reason, or nothing left to suppress           |
| `catalog`                 | Inherited from the lint                 | A `lintCatalog` convention violation                                             |

The command exits `1` when any error-severity issue is found. Warnings do not fail the build.

## Emitting the catalog

Before running `alfiz-verify`, emit your catalog to a JSON file that the CLI can load. Use `catalog.toDocument()`:

```bash theme={null}
node --experimental-strip-types -e \
  'import("./src/alfiz.ts").then(m => console.log(JSON.stringify(m.catalog.toDocument())))' \
  > alfiz-catalog.json
```

<Note>
  `--experimental-strip-types` needs **Node 22.6 or newer**. On an older runtime, compile first and run the emitter against the built output.
</Note>

If you use a build step that compiles TypeScript first, run from the compiled output:

```bash theme={null}
node -e \
  'import("./dist/alfiz.js").then(m => console.log(JSON.stringify(m.catalog.toDocument())))' \
  > alfiz-catalog.json
```

## Exit codes

| Code | Meaning                                                                                  |
| ---- | ---------------------------------------------------------------------------------------- |
| `0`  | Clean, with no errors (warnings may be present)                                          |
| `1`  | One or more error-severity issues found                                                  |
| `2`  | Config file missing or unparseable, or a `serverFilePatterns` entry is an invalid RegExp |

## Output format

Each issue is printed to stderr in the format:

```
<symbol> [<rule>] <file>:<line> — <message>
```

| Symbol | Meaning                                                     |
| ------ | ----------------------------------------------------------- |
| `✖`    | Error-severity issue                                        |
| `⚠`    | Warning-severity issue                                      |
| `○`    | Ignored file (skipped by `alfiz-verify-ignore-file` pragma) |

A summary line follows all issues:

```
alfiz-verify: 42 file(s), 1 ignored, 2 error(s), 1 warning(s)
```

## File-level opt-out

Files that authenticate outside the catalog by design (system trust domains, deploy-key surfaces, internal health endpoints) can opt out of scanning with a file-header pragma:

```ts theme={null}
// alfiz-verify-ignore-file system trust domain, authenticates by deploy key
"use server";

// ... no Alfiz gates here by design
```

The pragma must appear in the file's header region, before the first non-directive statement. Comments before or between `"use server"` / `"use client"` / `"use strict"` directives all qualify. The reason text is required: an unexplained exemption in a security tool is how exemptions rot.

<Warning>
  A pragma that appears after the header region does nothing. `alfiz-verify` reports it as a warning rather than silently dropping it, so you can see that the file is still being scanned.
</Warning>

## GitHub Actions example

```yaml theme={null}
# .github/workflows/verify.yml
name: alfiz-verify

on:
  push:
    branches: [main]
  pull_request:

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci

      - name: Emit catalog
        run: |
          node --experimental-strip-types -e \
            'import("./src/alfiz.ts").then(m => console.log(JSON.stringify(m.catalog.toDocument())))' \
            > alfiz-catalog.json

      - name: Run alfiz-verify
        run: npx alfiz-verify
```

## Built-in gate and visibility names

`alfiz-verify` recognizes the following names as gates and visibility affordances out of the box. The `gateNames` and `visibilityNames` config fields are **additive**, so your custom wrappers extend the defaults; the defaults are always included via the CLI.

**Default gate names:** `can`, `fresh`, `require`, `requirePermission`, `gateAction`, `apiRequirePermission`

**Default visibility names:** `canAny`, `requireAny`, `holds`

**Default server file patterns:** `app/.*route\.(t|j)sx?$`, `pages/api/`. The first is anchored, so it matches only paths ending in `route.ts(x)` / `route.js(x)`

<Tip>
  Property access is matched on the final name only. `session.require`, `client.can`, and `can.fresh` all count as gates, so you do not need to add them separately.
</Tip>
