Authoring and grants
How a screen says what it needs, why contract membership is not authorization, and what variants have to satisfy to be safe.
A server-driven document describes a screen without describing a query. A fragment says put the
agreement summary here through a capability:// reference, never an endpoint or a SQL statement.
That indirection is what lets one document be content-authored, device-cached and rendered three ways
without any of them widening what the viewer may see or do.
Publishing is not granting
A capability publishing a view proves the view exists. It does not prove this release may use it. Every reference a document uses must appear in the release's explicit grants.
grants: {
views: ["capability://agreements/summary"],
intents: ["capability://agreements/amend"],
}A reference is resolved wherever it occurs: at a prop key, inside an array, nested in either, or in an action's parameters. Arrays are the ordinary shape for list props, so that is the common case rather than an edge one.
Deriving grants from what an actor holds
Grants can be hand-maintained, and for a small deployment they often are. Past that point three places end up answering the same question. The shell decides what to show from its own configuration, the release carries a grants list somebody edited, and the platform enforces from the capability's declarations. The three drift, and the one the viewer sees is the one that drifts first.
deriveAuthorizationGrants makes the grants a projection of the same declarations. A capability's
usage contract can state what using it requires:
requirements: {
permissions: ["agreements.read"],
entitlements: ["plan.pro"],
featureFlags: ["agreements.v2"],
}Fabric does not administer any of those. They come from wherever entitlements already live, whether a licence, a role assignment, or an identity provider's scopes. What Fabric decides is what they add up to:
const { grants, withheld, unaddressable } = deriveAuthorizationGrants(contracts, {
permissions: claims.permissions,
entitlements: tenant.entitlements,
}, { document });A capability contributes nothing unless every requirement it declares is held. Partial access is not modelled, because a capability that publishes a view behind a permission has said the whole capability is gated, and inferring finer structure from its silence would be inventing policy.
Pass the document whenever the grants will be promoted. Validation carries grants onto the release unexamined and promotion compiles a signed route for every one, so a surplus grant becomes a signed route to a capability the screen never renders. Scoping to the document keeps the release's exposed surface equal to what it actually uses, across the default tree and every variant.
Silence is not permission. A contract that declares no requirements at all is withheld by default.
Requirements are optional metadata, so silence is the common case, and reading it as "available to
everyone" would make the default open. Pass undeclaredRequirements: "grant" only for a contract set
you have decided is unrestricted.
The result explains its own omissions, which is what makes an absent navigation entry debuggable rather than mysterious:
| List | What is in it | Why it was left out |
|---|---|---|
withheld | A capability and the first requirement the actor is missing | The actor does not hold what the capability requires |
unaddressable | A view reference and the versions it would have to choose between | The capability publishes that view at more than one version, and a capability:// reference carries no version, so validation would refuse the grant as ambiguous and promotion could not route it |
An intent whose action the contract does not declare is left out for the same reason as an unaddressable view. Promotion resolves only declared actions, so a grant for a dangling intent is one validation refuses and promotion would never have produced.
This is navigation-shaped, and it is not enforcement. It decides what a viewer is offered. The platform still re-checks authority when an action executes and still authorizes every query, so stale navigation is a cosmetic problem and never a security one. Nothing here is a gate.
Least privilege inside one page
A page assembled from several teams' fragments would otherwise give every fragment the union of
everything any of them needs. grants.fragments narrows a subtree, inherited by its children.
fragments: {
"payment-panel": { views: ["capability://settlements/balance"] },
}Narrowing is an intersection with the grants already in force. A fragment entry can only ever
narrow, so there is no path by which a subtree gains a reference the release did not grant. Naming
one that the release does not grant is reported as fragment_grant_exceeds_release rather than
silently ignored, because an entry that quietly does nothing is worse than one that fails.
Fragment ids are unique within a tree but may repeat across variants, so two variants sharing an id share an entry. Give them distinct ids when they need distinct narrowing.
Visibility is decided by view, never by field. Projection authorization answers allowed or denied for
a whole view, so a fact that one role may see and another may not is two views, not one view with a
hidden column. A dealer's cost breakdown and a homeowner's summary are proposal/cost-view and
proposal/customer-summary, and the homeowner's tree is simply never granted the first. Hiding the
field in a component is defense in depth; excluding the view from the grants is the control.
Variants have to be enumerated
Experiment arms and personalization are document.variants. Every selectable tree is declared,
validated against the same grants as the default tree, and covered by the release digest.
Enumeration is what makes the invariant enforceable. "Selection may never widen grants" is unenforceable as a rule if selection happens at request time, because you cannot validate a set you cannot enumerate. A selector that can produce a tree outside the release is a selector whose output was never checked.
One document, several brands
document.supportedTokenSets lets one authored document be promoted per brand. Each promotion pins
exactly one resolved token set, so a release digest still identifies precisely what a viewer
received.
Carrying several token sets inside a single release would author once at the cost of that property. It is far harder to regain than to keep.
Content systems
A content system authoring documents is expected and supported. It becomes an authorization surface
only if what it produces reaches a renderer without promotion. Run validateSduiRelease on every
publish, against the tenant's current grants. Grants are fixed per release, and a content system can
author a new document at any time.