PermDock
CLI

catalog

Export the permission catalog as JSON, JSON Schema or Markdown, and the shape of the catalog document.

permdock catalog turns the merged permission definition into documents that people, CI and agents read without running the app. It is the read-only sibling of collect: collect scans source and writes permissions.catalog.json; catalog renders that catalog (or the runtime definition directly) in the requested format.

Usage

permdock catalog --format json     > permissions.catalog.json
permdock catalog --format schema   > permissions.schema.json     # JSON Schema of the catalog document
permdock catalog --format markdown > docs/permissions.md
permdock catalog --from ./src/permissions.ts   # render straight from the definition module, skipping collect
permdock catalog --include post --include billing.invoice        # scope to a subtree

Where the data comes from

Permission leaves are plain JSON and the definition is a runtime value, so the catalog is produced by importing the definition module and walking listPermissions(permissions). Resource schemas are exported through Standard JSON Schema (~standard.jsonSchema.output(...)) when the validator supports it (Zod, Valibot and ArkType do); resources whose validator does not implement Standard JSON Schema get schema: null and a warning.

Usage sites and defining files come from the source scan; usages is always present and empty when no call site was found.

Catalog JSON shape

{
  "$schema": "https://permdock.com/schemas/catalog-v1.json",
  "version": 1,
  "generatedAt": "2026-09-06T10:00:00Z",
  "generator": "permdock@0.1.0",
  "resources": {
    "post": {
      "id": "id",
      "schema": {
        "type": "object",
        "properties": { "id": { "type": "string" } }
      },
      "definedIn": "features/posts/permissions.ts"
    }
  },
  "permissions": [
    {
      "key": "post.update",
      "scope": "post:update",
      "resource": "post",
      "action": "update",
      "arity": "instance",
      "meta": { "title": "Update post", "description": "Edit a post you own" },
      "usages": [
        { "file": "app/posts/[id]/page.tsx", "line": 42, "call": "assert" },
        { "file": "features/posts/policy.ts", "line": 12, "call": "allow" }
      ]
    },
    {
      "key": "post.create",
      "scope": "post:create",
      "resource": "post",
      "action": "create",
      "arity": "collection",
      "meta": {},
      "usages": []
    }
  ]
}

Notes on the shape:

  • permissions is a flat array sorted by key; nesting (billing.invoice.pay) is expressed in the dotted key, and resource holds the dotted resource path.
  • arity is instance or collection.
  • meta is whatever the definition attached to the action (title, description, tags, or any JSON); it is what MCP tool descriptions and Markdown output use.
  • schema is a JSON Schema document; the target (draft-2020-12, openapi-3.0) is selectable with --schema-target.
  • Without a configured policy, the catalog has no grants, role details or conditions. With one, grants lists every code grant with its portable conditions, approval, fields and validity, in a canonical order (wire formats); closures are marked portable: false and never serialised. diff compares that section between two versions.
  • The $schema URL is the published JSON Schema of the catalog format, which is the same document --format schema emits, so agents can validate a catalog offline. The exact hostname is fixed when the docs site launches.

Reading a catalog

A package that reads the catalog at run time imports permdock/catalog, a runtime entry with no CLI code. A build script finds the file with catalogPath(config, cwd) from permdock/cli.

import { readFileSync } from "node:fs";
import { catalogPath } from "permdock/cli";
import { parseCatalog, rowConditionKeys } from "permdock/catalog";
import config from "./permdock.config.ts";

const catalog = parseCatalog(
  readFileSync(catalogPath(config, process.cwd()), "utf8"),
);
const conditioned = rowConditionKeys(catalog);
const sqlEnforceable = catalog.permissions.filter(
  (permission) => !conditioned.has(permission.key),
);
  • parseCatalog(json) takes the JSON text or the parsed value. It checks it against schemas/catalog-v1.json and returns a deep-frozen copy built from own keys. A __proto__ key stays an ordinary key, and an object with any other prototype, a cycle or a value JSON cannot carry is rejected. On failure it throws PermDockValidationError with code: 'invalid-data', boundary: 'catalog' and every issue with its path.
  • rowConditionKeys(catalog) is the set of keys marked rowConditions: true: permissions whose code grants depend on more than role and scope, so a policy that calls only the SQL helpers would widen access (RLS).
  • catalogPath(config, cwd, out?) resolves the file the same way collect writes it: out, then collect.out, then catalog.out, then permissions.catalog.json.
  • The types (CatalogDocument, CatalogPermission, CatalogResource, CatalogRole, CatalogScope, CatalogGrant, CatalogValidity, CatalogApproval, CatalogBreakGlass, CatalogActivation, CatalogSupportAccess, CatalogUsage) are exported from both permdock/catalog and permdock/cli.

The reader, the $schema file and --format schema come from one schema constant, and a test checks the committed catalogs against both the reader and Ajv.

Markdown output

--format markdown renders one section per resource with a table of actions, arity, scope and metadata, and (when present) usage counts. It is intended to be committed under docs/ or pasted into a PR so reviewers can see the permission surface change without reading TypeScript.

Consumers

  • openapi reads the catalog to emit securitySchemes scopes and x-permdock-permissions.
  • The MCP adapter uses meta for tool descriptions and arity to decide whether a data loader is required.
  • The docs MCP server (search, list_pages and get_page at /mcp) and llms.txt expose the Markdown output so agents can ask "which permissions exist?".
  • permdock/testing compares a policy matrix against the catalog to find permissions with no test.

Last updated on

On this page