defineCatalog. All input types are imported from @alfiz/core.
CatalogInput
The top-level object passed to defineCatalog.
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.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 below. Group levels are inferred from the keys; nothing declares them.Record<string, ImportInput>
Permissions this application references but does not own, keyed by the foreign namespace, which must not be one this catalog owns. See imported permissions.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.Record<string, GroupInput>
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.Record<string, ScopeTypeInput>
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").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.{ depth?: number | "any" }
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.boolean
default:"true"
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.PermissionsInput
What the permissions field accepts.
LeafMap maps full dotted keys to their leaf declarations, which is the whole catalog for a small app:
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.
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.
string
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.string
Longer help text shown beside the label in role editors and request forms.
"read" | "action"
The read-versus-action taxonomy. When omitted,
defineCatalog infers it: "read" for leaves named read or read_*; "action" for everything else.boolean
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.readonly ScopeType[]
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.boolean
default:"false"
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.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.
string
Short human-facing name for the group in admin UIs. Falls back to the group’s path segment when absent.
string
Longer description for the group, shown in role editors and audit surfaces.
readonly ScopeType[]
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.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.
string
Human-readable description of the resource kind, shown in admin UIs and request forms.
ScopeType | null
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.boolean
default:"false"
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.object
Declares that grants at instances of this scope type are requestable. When absent, access at this scope type cannot be requested.
readonly RequestPromptInput[]
Structured justification prompts shown on the request form.
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.number
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.
boolean
default:"false"
When
true, the request must propose an expiry, so open-ended access cannot be requested for this scope 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.ApprovalStage is a closed union of exactly those three kinds. See
approval policies for the four
AutoApprovalPredicate shapes.A role declares its requestability differently:
RoleInput.requestable
carries stages directly, with no enclosing policy key. Scope types nest
theirs under policy.NavItemInput
Navigation items wire permission keys to sidebar entries. Each item’s permission controls whether the item is visible to the current user.
string
required
The display label for this navigation item.
string
The URL this item links to. Omit for section headers that group children without linking anywhere.
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.readonly NavItemInput[]
Nested child nav items. Supports arbitrary nesting depth.
Derived types
KeyOf<Cat>
All concrete permission keys of the catalog as a TypeScript union:
PatternOf<Cat>
All valid patterns: concrete keys, group wildcards, and the bare "*":
ScopeOf<Cat>
Every scope id the catalog’s declared scopeTypes can name, plus the global scope:
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.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: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:
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:
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: