> ## 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 Catalog TypeScript Types: CatalogInput Reference

> Reference for CatalogInput, PermissionLeafInput, GroupInput, ScopeTypeInput, NavItemInput, and the derived KeyOf and PatternOf types used throughout Alfiz.

This page covers every TypeScript type used when declaring a catalog with `defineCatalog`. All input types are imported from `@alfiz/core`.

```ts theme={null}
import type {
  CatalogInput,
  PermissionsInput,
  PermissionLeafInput,
  PermissionBlock,
  GroupInput,
  ScopeTypeInput,
  NavItemInput,
} from "@alfiz/core";
```

## `CatalogInput`

The top-level object passed to `defineCatalog`.

<ParamField body="namespaces" type="readonly string[]" required>
  The namespaces your application owns; the first is the primary. Each must be a single valid segment matching `/^[a-zA-Z][a-zA-Z0-9_]*$/`, and every permission key must begin with one of them. `alfiz_internal` is reserved.

  Catalogs are federation-shaped from the first commit: declaring namespaces explicitly means the shape is portable to a federated deployment without a later refactor.
</ParamField>

<ParamField body="permissions" type="PermissionsInput" required>
  The permissions, keyed by their full dotted key. Accepts a flat map, a single `group()` block, or an array mixing both. See [`PermissionsInput`](#permissionsinput) below. Group levels are inferred from the keys; nothing declares them.
</ParamField>

<ParamField body="imports" type="Record<string, ImportInput>" optional>
  Permissions this application **references but does not own**, keyed by the foreign namespace, which must not be one this catalog owns. See [imported permissions](/catalog/imports).

  ```ts theme={null}
  interface ImportInput {
    from?: string;                       // provenance, e.g. "registry:zoom@^3"
    document?: CatalogDocument;          // the owner's published catalog
    scopes?: readonly ScopeType[];       // YOUR scope types, never theirs
    permissions: ImportedPermissionsInput;
    strict?: boolean;                    // close an undocumented wildcard
  }
  ```

  Attaching `document` is the recommended shape: wildcards expand, `canAny` answers exactly, and a typo fails the build. Without it a wildcard is an **opaque region**: grantable and checkable, but approximated fail-closed wherever an answer would need expanding a pattern into keys. `strict: true` closes the admission half of an undocumented region.
</ParamField>

<ParamField body="groups" type="Record<string, GroupInput>" optional>
  Metadata for group paths not covered by a `group()` block, typically the project level. Purely optional decoration; it cannot bring a group into existence, since groups come from the keys.
</ParamField>

<ParamField body="scopeTypes" type="Record<string, ScopeTypeInput>" optional>
  Scope type declarations. Required for any catalog that grants permissions at non-global resource scopes. Keys are dotted identifiers matching the full key grammar (e.g., `"docs.folder"`, `"docs.doc"`).
</ParamField>

<ParamField body="navigation" type="readonly NavItemInput[]" optional>
  The sidebar navigation tree. Each item's `permission` controls its visibility. References are validated by `alfiz-verify`, so nav items that reference unknown permissions produce errors.
</ParamField>

<ParamField body="conventions" type="{ depth?: number | &#x22;any&#x22; }" optional>
  House conventions enforced by `lintCatalog` and `alfiz-verify`. `depth` is the key depth `lintCatalog` checks for, defaulting to `3`; set another number for a different house style, or `"any"` to opt out.

  Deviations are **lint errors and not boot errors**, so a two-level integration catalog builds and CI reports it.
</ParamField>

<ParamField body="includeAlfizInternal" type="boolean" default="true" optional>
  When `true` (the default), Alfiz's own `alfiz_internal.*` permissions are merged into the catalog. Set to `false` only for catalogs that render no Alfiz administration UI. When false, all `alfiz_internal.*` features, including view-as, are unavailable.
</ParamField>

***

## `PermissionsInput`

What the `permissions` field accepts.

```ts theme={null}
type LeafMap = Record<string, LeafInput>;

type PermissionsInput =
  | LeafMap
  | PermissionBlock
  | readonly (LeafMap | PermissionBlock)[];
```

A **`LeafMap`** maps full dotted keys to their leaf declarations, which is the whole catalog for a small app:

```ts theme={null}
permissions: {
  "docs.files.read": { kind: "read" },
  "docs.files.delete": { destructive: true },
}
```

A **`PermissionBlock`** is what `group()` returns: one group's metadata plus the keys under it. Use blocks to organize a large catalog into named, foldable units; they are never required.

```ts theme={null}
function group<P extends string, L extends LeafMap>(
  path: P,
  meta: GroupInput,
  leaves: L,          // every key must start with `${P}.`, a compile error otherwise
): PermissionBlock<P, L>;

// The metadata argument is optional:
function group<P extends string, L extends LeafMap>(path: P, leaves: L): PermissionBlock<P, L>;
```

An **array** mixes both, which is how a catalog composed from per-feature files is assembled:

```ts theme={null}
permissions: [files, sharing, { "docs.reports.read": { kind: "read" } }]
```

***

## `PermissionLeafInput`

A permission leaf is a concrete, grantable and checkable permission: the value assigned to a full dotted key in a `LeafMap`. The shorthand `true` is equivalent to an empty `PermissionLeafInput {}` and lets `defineCatalog` infer all fields.

```ts theme={null}
type LeafInput = true | PermissionLeafInput;
```

<ParamField body="label" type="string" optional>
  Short human-facing name for pickers and checkboxes, e.g. `"Publish file"`. Falls back to the permission name when absent. Keep it brief, since the `description` field carries longer help text.
</ParamField>

<ParamField body="description" type="string" optional>
  Longer help text shown beside the label in role editors and request forms.
</ParamField>

<ParamField body="kind" type="&#x22;read&#x22; | &#x22;action&#x22;" optional>
  The read-versus-action taxonomy. When omitted, `defineCatalog` infers it: `"read"` for leaves named `read` or `read_*`; `"action"` for everything else.
</ParamField>

<ParamField body="destructive" type="boolean" optional>
  Marks the permission as a destructive action that should be gated with `can.fresh()` (cache-bypassing) at enforcement points. When omitted, inferred `true` for leaves named `delete`, `delete_*`, `destroy`, `destroy_*`, `purge`, or `purge_*`; `false` for everything else.
</ParamField>

<ParamField body="scopes" type="readonly ScopeType[]" optional>
  Scope types at which this permission is grantable, **in addition to the global scope `"*"`**. When omitted, inherits from the nearest enclosing group that declares `scopes` (a `group()` block or a `groups` entry); falls back to `[]` (global-only) when no ancestor declares scopes. Declare explicitly (including `[]` for global-only) to override the inherited default. Granting at an undeclared scope type is a validation error at write time.
</ParamField>

<ParamField body="impliedOnAncestors" type="boolean" default="false" optional>
  Ancestor visibility: when `true`, holding any grant of this leaf at a scope implies it on the **proper ancestors** of that scope. This enables the "a shared document shows its containing folder" pattern, where a user granted access to `docs.doc:123` also passes `docs.files.read` at the folder that contains it. Off by default.

  The implication deliberately stops short of the global scope: an unscoped `can(user, key)` is the *everywhere* question, and a grant at one document must never answer it. `explain()` reports an implied allow with `implied: true` and names the descendant grants in `impliedBy`, leaving `matchedGrants` empty, because nothing matched *at this scope*, which is what makes it implied.
</ParamField>

***

## `GroupInput`

The metadata a group path carries. Groups are folders in the permission tree and never carry keys themselves; only their leaves do. Crucially, `GroupInput` does **not** create a group: every dotted prefix of a declared key is already one. This only decorates it, via a `group()` block or the catalog's `groups` map.

```ts theme={null}
export interface GroupInput {
  label?:       string;
  description?: string;
  scopes?:      readonly ScopeType[];
}
```

<ParamField body="label" type="string" optional>
  Short human-facing name for the group in admin UIs. Falls back to the group's path segment when absent.
</ParamField>

<ParamField body="description" type="string" optional>
  Longer description for the group, shown in role editors and audit surfaces.
</ParamField>

<ParamField body="scopes" type="readonly ScopeType[]" optional>
  Default scope types for every leaf under this group (including leaves in descendant groups), overridable per leaf or by a nearer enclosing group. Saves declaring an identical `scopes: [...]` on dozens of sibling leaves when a whole tab is scoped to one resource type.
</ParamField>

***

## `ScopeTypeInput`

A scope type is the static schema fact declaring a resource kind. Instances of the type (e.g., `docs.folder:abc123`) are the runtime resource IDs stored in grants.

<ParamField body="description" type="string" optional>
  Human-readable description of the resource kind, shown in admin UIs and request forms.
</ParamField>

<ParamField body="parent" type="ScopeType | null" optional>
  The expected parent scope type. `null` for top-level types whose instances parent directly to `"*"`. A type whose instances nest under other instances of the same type (folders in folders) declares itself as its own parent: `{ parent: "docs.folder" }`.

  This is a **commitment**, not a hint. A `parent: null` type's instances have the ancestor chain `[scope, "*"]` by declaration, which lets the request-scoped snapshot check them synchronously without consulting the ancestry resolver.

  When omitted, the parent defaults to `null`.
</ParamField>

<ParamField body="multiParent" type="boolean" default="false" optional>
  When `true`, an instance's effective access is the **union** of all parents' access (e.g., a document in multiple folders). Off by default. Some products need this (shortcuts, labels-as-folders); others consider it a leak vector. Enable it explicitly.
</ParamField>

<ParamField body="requestable" type="object" optional>
  Declares that grants at instances of this scope type are requestable. When absent, access at this scope type cannot be requested.

  <ParamField body="requestable.prompts" type="readonly RequestPromptInput[]" optional>
    Structured justification prompts shown on the request form.

    ```ts theme={null}
    interface RequestPromptInput {
      id: string;
      label: string;
      kind?: "text" | "select";     // default "text"
      options?: readonly string[];  // for "select" prompts
      required?: boolean;
    }
    ```

    `submitRequest` validates answers against these prompts server-side.
    required prompts must be answered, and a `select` answer must be one of
    the declared `options`, so a hand-rolled form cannot skip one.
  </ParamField>

  <ParamField body="requestable.maxDurationMs" type="number" optional>
    Maximum grant duration a request may propose, in milliseconds. When the requester omits a proposed expiry, this cap becomes the expiry automatically. When the requester proposes an expiry exceeding this value, the request is rejected.
  </ParamField>

  <ParamField body="requestable.requireExpiry" type="boolean" default="false" optional>
    When `true`, the request must propose an expiry, so open-ended access cannot be requested for this scope type.
  </ParamField>

  <ParamField body="requestable.policy" type="ApprovalPolicyInput" required>
    The approval workflow. Must declare at least one `stage`. A requestable scope type with an empty `stages` array is a catalog error, caught by `alfiz-verify`.

    ```ts theme={null}
    interface ApprovalPolicyInput {
      stages: readonly ApprovalStage[];
    }

    type ApprovalStage =
      | { kind: "auto"; predicate: AutoApprovalPredicate }
      | { kind: "named_approvers"; roleId: string }
      | { kind: "management"; layers?: number };  // 1 = direct manager
    ```

    `ApprovalStage` is a closed union of exactly those three kinds. See
    [approval policies](/requests/approval-policies) for the four
    `AutoApprovalPredicate` shapes.

    <Note>
      A **role** declares its requestability differently: `RoleInput.requestable`
      carries `stages` directly, with no enclosing `policy` key. Scope types nest
      theirs under `policy`.
    </Note>
  </ParamField>
</ParamField>

***

## `NavItemInput`

Navigation items wire permission keys to sidebar entries. Each item's `permission` controls whether the item is visible to the current user.

<ParamField body="label" type="string" required>
  The display label for this navigation item.
</ParamField>

<ParamField body="href" type="string" optional>
  The URL this item links to. Omit for section headers that group children without linking anywhere.
</ParamField>

<ParamField body="permission" type="PermissionPattern | readonly PermissionKey[]" required>
  Visibility wiring: a concrete key, an array of keys (any-of), or a subtree pattern (evaluated via `canAny`). This controls visibility only. The target page still gates its own reads. `alfiz-verify` validates that every referenced key or pattern exists in the catalog.
</ParamField>

<ParamField body="children" type="readonly NavItemInput[]" optional>
  Nested child nav items. Supports arbitrary nesting depth.
</ParamField>

***

## Derived types

### `KeyOf<Cat>`

All concrete permission keys of the catalog as a TypeScript union:

```ts theme={null}
import type { KeyOf } from "@alfiz/core";
import { catalog } from "./alfiz.js";

type AppKey = KeyOf<typeof catalog>;
// → "docs.files.read" | "docs.files.update_file" | "docs.files.delete" | ...
```

### `PatternOf<Cat>`

All valid patterns: concrete keys, group wildcards, and the bare `"*"`:

```ts theme={null}
import type { PatternOf } from "@alfiz/core";
import { catalog } from "./alfiz.js";

type AppPattern = PatternOf<typeof catalog>;
// → "*" | AppKey | "docs.*" | "docs.files.*" | ...
```

### `ScopeOf<Cat>`

Every scope id the catalog's declared `scopeTypes` can name, plus the global scope:

```ts theme={null}
import type { ScopeOf } from "@alfiz/core";
import { catalog } from "./alfiz.js";

type AppScope = ScopeOf<typeof catalog>;
// → "*" | `docs.folder:${string}` | `docs.doc:${string}`
```

<Note>
  Keys and patterns **gate**: a typo at a literal call site is a compile error. Scope ids **hint** (`LooseScopeId<S>` is `S | (string & {})`): a literal scope autocompletes every declared `<scopeType>:` prefix, while an id built from a variable or a database row flows through unchanged. The instance half of a scope id is runtime data by nature; a permission key never is.
</Note>

### The carrier aliases

Four aliases exist so a value stored on a context object needs no hand-written type parameters. Each threads all three unions through:

| Alias                    | Resolves to                                                      | Import from          |
| ------------------------ | ---------------------------------------------------------------- | -------------------- |
| `ClientOf<Cat>`          | `AlfizClient<KeyOf<Cat>, PatternOf<Cat>, ScopeOf<Cat>>`          | `@alfiz/core`        |
| `SnapshotOf<Cat>`        | `AlfizSnapshot<KeyOf<Cat>, PatternOf<Cat>, ScopeOf<Cat>>`        | `@alfiz/core`        |
| `SessionOf<Cat>`         | `AlfizSession<KeyOf<Cat>, PatternOf<Cat>, ScopeOf<Cat>>`         | `@alfiz/application` |
| `SessionSnapshotOf<Cat>` | `AlfizSessionSnapshot<KeyOf<Cat>, PatternOf<Cat>, ScopeOf<Cat>>` | `@alfiz/application` |

```ts theme={null}
import type { ClientOf, SnapshotOf } from "@alfiz/core";
import type { SessionOf, SessionSnapshotOf } from "@alfiz/application";
import { catalog } from "./alfiz.js";

type RequestContext = {
  alfiz:   ClientOf<typeof catalog>;
  snap:    SnapshotOf<typeof catalog>;
  session: SessionOf<typeof catalog>;
};
```

### Types for the published document

`defineCatalog` derives its unions from the catalog *literal*. Code that consumes the published `CatalogDocument` instead, such as a federated sibling or another repo, gets the same treatment through codegen:

```bash theme={null}
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";

// TypedCatalog<K, P, S>: createAlfizClient picks the unions straight up.
const catalog = catalogFromDocument<AlfizKey, AlfizPattern, AlfizScopeId>(doc);
```

An untyped `catalogFromDocument(doc)` returns a `string`-typed catalog, honestly, rather than pretending to a precision it does not have.

### `AlfizInternalKey`

The union of all `alfiz_internal.*` permission keys:

```ts theme={null}
import type { AlfizInternalKey } from "@alfiz/core";
// → "alfiz_internal.access.read"
//   | "alfiz_internal.access.manage_roles"
//   | "alfiz_internal.access.manage_groups"
//   | "alfiz_internal.access.manage_grants"
//   | "alfiz_internal.access.manage_revokes"
//   | "alfiz_internal.access.manage_reporting"
//   | "alfiz_internal.access.view_as"
//   | "alfiz_internal.requests.read"
//   | "alfiz_internal.requests.decide_request"
//   | "alfiz_internal.audit.read"
//   | "alfiz_internal.catalog.read"
//   | "alfiz_internal.catalog.publish_catalog"
```

***

## `ALFIZ_INTERNAL_BLOCKS`

The built-in administration blocks merged into every catalog where `includeAlfizInternal !== false`, exported as a readonly array of four `group()` blocks. Their permission leaves are:

### `alfiz_internal.access`

Roles, groups, grants, revokes, hierarchy, and view-as.

| Leaf               | Kind   | Description                                          |
| ------------------ | ------ | ---------------------------------------------------- |
| `read`             | read   | View the access administration surface               |
| `manage_roles`     | action | Create, edit, delete roles                           |
| `manage_groups`    | action | Create, edit, delete user groups and their parentage |
| `manage_grants`    | action | Create and delete grants                             |
| `manage_revokes`   | action | Create and delete personal revokes                   |
| `manage_reporting` | action | Edit reporting (manager) edges                       |
| `view_as`          | action | Preview the product as a role or an individual       |

### `alfiz_internal.requests`

Access requests and approvals.

| Leaf             | Kind   | Description                       |
| ---------------- | ------ | --------------------------------- |
| `read`           | read   | View the approvals inbox          |
| `decide_request` | action | Approve or deny an access request |

### `alfiz_internal.audit`

The audit log.

| Leaf   | Kind | Description        |
| ------ | ---- | ------------------ |
| `read` | read | Read the audit log |

### `alfiz_internal.catalog`

Catalog administration.

| Leaf              | Kind   | Description                                |
| ----------------- | ------ | ------------------------------------------ |
| `read`            | read   | View the published catalog                 |
| `publish_catalog` | action | Publish a verified catalog to the provider |
