defineCatalog is where your authorization model lives. It takes a structured declaration of your permission tree and produces a Catalog object that the Application validates writes against, the client checks keys against, and the build-time verifier consumes. The catalog is declared in code, never configured in a dashboard and never inferred from call sites, so it is always the ground truth.
Permissions are declared by their full dotted key, the same notation every check, grant, role pattern, and nav entry uses, and group levels are inferred from those keys. A small catalog is one flat map; a large one composes from group() blocks.
defineCatalog throws CatalogError immediately on structural invalidity (bad segments, undeclared namespaces, missing scope types, a key that is also a group path). Convention violations such as key depth, the naming floor and nav wiring are reported by alfiz-verify at build time, not at runtime.
Signature
Parameters
readonly string[]
required
The namespaces your application owns, being the first segment of every permission key it declares. The first entry is the primary namespace. Each must be a single valid segment (
[a-zA-Z][a-zA-Z0-9_]*); alfiz_internal is reserved.Catalogs are federation-shaped from the first commit, so this is required even standalone, where it is locally redundant. Example: ["docs"] produces keys like docs.files.read; a multi-project portal declares ["docs", "billing", "admin"].This is what your application owns. Permissions it merely references belong in imports.Record<string, ImportInput>
Permissions your application references but does not own, keyed by the foreign namespace, whether from Alfiz Cloud or a federated application. See Imports for the full treatment.Each import declares specific keys or subtree patterns, never the bare
*. Importing a namespace you own, declaring an entry outside the namespace it names, or wiring one to a scope type this catalog does not declare all throw CatalogError. With a document attached, an entry matching nothing the owner publishes throws too, which is the local half of drift detection.strict: true closes the admission half of an undocumented wildcard: without it, an opaque region admits any key matching its pattern, since there is no published document to check the key against. Set it when you import a wildcard you have no document for and still want a typo to fail.PermissionsInput
required
The permissions, keyed by their full dotted key. Accepts a flat map, a single Group levels are inferred from the keys, so every dotted prefix of a key is a group. Values are
group() block, or an array mixing both:true (all defaults) or a PermissionLeafInput. See Catalog Types for the full field reference.Record<string, GroupInput>
Metadata (
label, description, scopes) for group paths you did not declare a group() block for, typically the project level, e.g. { docs: { label: "Documents" } }. Purely optional: an undecorated group falls back to its path segment.Record<string, ScopeTypeInput>
Scope types this catalog uses. Required for any catalog that grants permissions at resource scopes (anything other than global
"*"). Keys are dotted identifiers like "docs.folder" or "docs.doc", and the first segment must be one of the catalog’s declared namespaces, or defineCatalog throws CatalogError. A bare "folder" is rejected for the same reason a bare permission key is.See Catalog Types for the full ScopeTypeInput field reference.Sidebar / nav tree wiring. Each item declares a
permission (a key, array of keys, or subtree pattern) that controls visibility of that nav item. The alfiz-verify linter checks that all referenced permissions exist in the catalog.See Catalog Types for the full NavItemInput field reference.{ depth?: number | "any" }
House conventions the linter enforces.
depth is the key depth lintCatalog checks for, defaulting to 3; set 2 for a thin integration catalog (zoom.host), another number for a deeper tree, or "any" to opt out.This is a naming preference, not a rule of the system. Nothing about evaluation, grants, patterns, or types reads it. lintCatalog is the only consumer, and a deviation is a CI finding rather than a boot-time throw. Set it to match your keys instead of renaming them to match it.boolean
default:"true"
When
true (the default), Alfiz’s own administration permissions are included under alfiz_internal.*. Set to false only for catalogs that render no Alfiz admin UI surface. When false, view-as and all other alfiz_internal.* features are unavailable, so createSession will deny any view-as attempt because the permission key does not exist.Return value
Catalog<C>
A
Catalog object carrying:catalog.leaves, aReadonlyMap<string, LeafMeta>of all permission keyscatalog.groups, aReadonlyMap<string, GroupMeta>of all group pathscatalog.scopeTypes, aReadonlyMap<string, ScopeTypeMeta>catalog.navigation, the built nav treecatalog.namespace/catalog.namespacescatalog.toDocument()emits the JSON wire shape foralfiz-verify
catalog.$key and catalog.$pattern carry the derived CatalogKeys<C> and CatalogPatterns<C> union types. Extract them with KeyOf<typeof catalog> and PatternOf<typeof catalog>.catalog.toDocument()
toDocument() serializes the catalog to the stable CatalogDocument wire shape consumed by alfiz-verify and the provider’s catalog publish endpoint.
--experimental-strip-types needs Node 22.6 or newer. On an older runtime, compile first and run the emitter against the built output.Template-literal types
Importing the catalog and usingKeyOf / PatternOf gives you compile-time verified permission strings everywhere:
includeAlfizInternal: true: built-in admin permissions
By default, defineCatalog merges the alfiz_internal project into the catalog. This adds the following permission keys, which gate Alfiz’s own headless administration components:
alfiz_internal.* keys are exempt from coverage linting in alfiz-verify, because they are gated inside Alfiz’s own admin surfaces, not your application code, so unreferenced warnings are suppressed for them automatically.