Skip to main content
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

Usage

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

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:
It is also how a wildcard import 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.

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:

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:
alfiz_internal.* keys are exempt from coverage warnings, because they are gated inside Alfiz’s own admin surfaces, not your application code.

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

4. Catalog linting

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

5. Import validation

Permissions from a namespace your catalog neither owns nor 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:
Configure the severity with importSource and implicitImports, 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:

Rule inventory

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():
--experimental-strip-types needs Node 22.6 or newer. On an older runtime, compile first and run the emitter against the built output.
If you use a build step that compiles TypeScript first, run from the compiled output:

Exit codes

Output format

Each issue is printed to stderr in the format:
A summary line follows all issues:

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

GitHub Actions example

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