Skip to main content
Your catalog declares what your application owns. Sometimes it also needs to reference permissions it does not own: a connector’s capabilities, or another application’s keys that your roles bundle alongside your own. namespaces and imports are the two halves of that:
  • namespaces. Prefixes you own. You define these keys, and you publish them.
  • imports. Prefixes you reference. Another application defines them; you name the slice you interface with, and wire it to your own resources.
Both live in the same catalog module, in code, for the same reason everything else does.
src/authz/catalog.ts
From here the catalog behaves as it always does. zoom.host is a typed key, alfiz-verify checks call sites that use it, and a role can bundle it with your own:

Attach the published document

An import resolves against the namespace owner’s published catalog. Fetch it in CI and commit it, exactly as you already commit the output of catalog.toDocument():
Attaching it is the recommended pattern, and the difference is concrete. A region still works. It is grantable, checkable and storable, but it is an approximation, and every approximation errs toward showing less. A revoke overlapping any part of a region suppresses the whole region for visibility checks. If you must import without a document, strict: true closes the admission half: the wildcard still declares grantable vocabulary, but can() on a key you cannot name throws rather than being evaluated.
An import declares specific keys or subtree patterns, and never the bare *. Importing everything from every namespace is not a contract.

Scope types are yours, not theirs

scopes on an import names scope types your catalog declares:
Writing scopes: ["zoom.meeting"] is a build error even though Zoom really does declare that scope type. The reason is structural: resolving a scope’s ancestor chain requires the resolver of the application that owns the resource, and your application does not have Zoom’s. Scope types you can resolve are the only ones a check here can honestly use. The default is [], which is grantable at the global scope only. Wire an import to a scope type when your own resources are what narrows it, as docs.folder does above.

Import the narrowest thing that works

A pattern broader than what you imported is rejected:
zoom.* is a claim over a namespace you do not own, and forward-inclusive wildcards mean a stored one would absorb every future Zoom permission. Declaring the slice you interface with keeps that from happening by accident, and keeps a role editor from storing it.

Typed keys across the boundary

A concrete import contributes its keys to KeyOf<typeof catalog> as literals, exactly like your own. A wildcard import cannot: a subtree you cannot enumerate has no closed key set, so it widens the union by one template member (`zoom.meetings.${string}`). That is enough to autocomplete the prefix and not enough to catch a typo in the last segment. Close it with codegen, the same mechanism that types a published catalog anywhere else:
Now zoom.meetings.reed fails to compile.

Publishing: what you announce vs. what you consume

catalog.toDocument() carries owned vocabulary only. Imported keys are never in it, because publishing them would mean defining keys in a namespace you do not own, which the registry’s namespace ownership exists to prevent. What you consume publishes separately:
They are different contracts. The first is vocabulary others may write grants against; the second is a dependency others can only warn you about, and that warning is what it buys you. The drift report can name roles and grants that reference a tombstoned key, but without manifests it can never name the code that still imports one.

Checking a permission you never declared

A check for a permission in a namespace you neither own nor import is an implicit import. It is a missing line rather than a topology, and both the verifier and the runtime say so. In CI, alfiz-verify reports the implicit-import rule:
The severity is an error when no import source is configured, since an application that has never imported anything has no plausible source for a foreign key and it is therefore a typo. It is a warning when one is configured, naming the declaration to paste. You usually get the warning without configuring anything: importSource defaults to "registry" as soon as your catalog declares any import at all. Override it in alfiz-verify.config.json:
alfiz-verify.config.json
Set "implicitImports": "off" to suppress the rule entirely, "implicitImportAllow": ["stripe"] to exempt one namespace, or suppress a single call site in place:
The rule name is required, so a per-line ignore can never quiet a finding it was not written for. At runtime, the check raises UnknownPermissionError unless you opt in:
Two things this never relaxes, whatever you set it to:
  • A permission under a namespace you own. Your catalog is enumerable, so an unknown key in it is a typo, and no configuration should hide one.
  • A permission outside an enumerated import. That import knows its own keys, so it tells you which ones, with a did-you-mean.
And one thing it never does: I/O. There is no provider lookup, no lazy fetch, no registry call. Alfiz is never in your request path, and snapshot.can() is synchronous, which makes a fetch-on-unknown design impossible there by construction.
A permission admitted this way is declared in no catalog, so a bare global * grant does not confer it, because * means everything in the declared vocabulary. A grant naming the namespace (zoom.*, or narrower) confers it normally. Without that rule, a typo in a foreign namespace would pass for exactly the broadly-privileged users who review and test the gate, and deny everyone it was written for.

Next

The catalog registry

Where a namespace is published, versioned, and tombstoned.

Federated

Cross-application roles, and what composes them.