restricted and it runs only for callers that present a grant from your node: a signed statement that a given key may call given functions until an expiry.
Grants are capability tokens, not an access list. Your node doesn’t keep a list of who may call. It checks the signatures on whatever grant the caller sends, so a caller can hand a narrower slice of its access to someone else without asking you.
Restriction and payment are independent. A function can be restricted, priced, both, or neither. A restricted, priced function needs both a grant and a payment voucher. See Payments.
Section 7.2 of the whitepaper proposes allow and deny lists called access manifests.
stellarium-ts implements restriction with the grants described here instead.The pieces
Walkthrough
This walkthrough restricts thecom.example.Weather resource from the Quickstart, then gives one caller node access to it.
1
Mark the function restricted
On the provider, add The flag is published with the resource, so callers see
restricted: true to the function:restricted: true in GET /v1/resources during discovery. From now on the provider answers any call to temperature without a valid grant with 403.2
Find the caller's public key
A grant is addressed to a key. The caller node’s key is its
SIGNING_PUBLIC_KEY, which it also serves at GET /v1/self:3
Issue a grant on the provider
Call Grants are signed, so the chain is safe to send over any channel. Only the audience’s private key can use it.
issueGrant with the caller’s key, the capabilities to grant, and how long each copy lasts:provider.ts
chain is an array holding one root grant, signed by the provider:4
Give the chain to the caller
On the caller node, store the chain with Or pass it in the start options. A chain passed this way is stored at start, and a newer renewed copy already in the database is kept:
addGrant, which checks its signatures and that it is addressed to this node:caller.ts
caller.ts
5
Call the function
Nothing changes in the script. When a request calls a restricted remote function, the caller node finds a held chain from that provider that covers the call, signs an authorization for it, and sends both:If the caller holds no grant that covers the call, it doesn’t contact the provider. The call evaluates to an
error value in the script:What the provider checks
For a restricted function, the provider runs the function only if all of these hold:- The first grant was issued by the provider’s own key.
- Every grant’s signature is valid, and none has expired.
- Each later grant was issued by the previous grant’s audience, names it as its
parent, expires no later than it, and allows nothing it doesn’t. - The authorization is signed by the last grant’s audience, covers this exact resource, function, and parameters, and is under 2 minutes old.
- The authorization’s nonce hasn’t been used before, so a captured authorization can’t be replayed.
- The last grant has a capability that matches the call.
- No grant in the chain has been revoked, and no audience in the chain is blocked.
Writing capabilities
A grant holds one or more capabilities. A call is allowed when any one of them matches.400) whose parameters repeat a name or use a name the function doesn’t declare. That way a constraint always checks the same value the function receives.
Letting callers ask for grants
Issuing every grant by hand doesn’t scale. Pass anapproveGrant hook to start and callers can request grants themselves at POST /v1/grants/request. The hook receives the caller’s key, the capabilities it asked for, and any details it sent, such as an invite code. Return the grant’s options to approve, or undefined to refuse:
provider.ts
capabilities narrower than the ones requested. The node checks the request’s signature and freshness before calling the hook, and passes each signed request to the hook at most once, so the hook can safely use up single-use codes.
On the caller, requestGrant signs the request, sends it, and stores the grant it gets back. It throws if the provider refuses:
caller.ts
403 for a restricted call includes a grantEndpoint field, so callers know where to ask. Without a hook, the provider refuses every request and only grants issued with issueGrant exist.
Delegating a grant
A node that holds a grant can issue a narrower one to another key withdelegateGrant. Name the held grant by the id of the last grant in its chain:
caller.ts
addGrant like any other chain.
Delegation can only narrow access:
- Every delegated capability must be covered by one in the parent grant. A narrower path, an added resource or function name, or an added constraint is fine. Dropping a constraint the parent has is not.
- A delegated grant never expires later than its parent.
- A chain holds at most four grants, the root included.
Expiry and renewal
Each copy of a grant lastslifetimeMs, at most 30 days. Access outlives one copy through renewal: each node renews the grants it holds once they are past half their lifetime, by asking the grant’s issuer at issuerBaseUrl for a fresh copy (POST /v1/grants/renew). The renewed copy keeps the same id.
The node checks for grants due for renewal every
grantRefreshIntervalMs (start option, 5 minutes by default), so calls don’t wait on renewal. If every held grant that covers a call has expired anyway, the node renews one before giving up. A held grant whose issuer still refuses to renew it 30 days after it expired is deleted.
Revoking grants and blocking callers
The provider has two ways to cut access off. Revoking refuses one grant, and everything delegated from it, in every call to the provider, and stops it being renewed. Revocation is permanent. Blocking refuses a caller’s key wherever the provider can identify it: anywhere in a restricted call’s chain, in grant requests and renewals, and in paid calls, where the voucher carries the payer’s key (403 Caller is blocked). Blocking a delegator also cuts off everything it handed on. Unblocking restores access.
Limits and responses
Authorization nonces are stored in the node’s SQLite database, so they survive a restart. Replicas of one node that don’t share a database don’t share nonces either.