Skip to main content
By default, any node that can reach yours can call any function you register. Mark a function 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 the com.example.Weather resource from the Quickstart, then gives one caller node access to it.
1

Mark the function restricted

On the provider, add restricted: true to the function:
The flag is published with the resource, so callers see 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 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:
Grants are signed, so the chain is safe to send over any channel. Only the audience’s private key can use it.
4

Give the chain to the caller

On the caller node, store the chain with addGrant, which checks its signatures and that it is addressed to this node:
caller.ts
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:
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.
The provider checks the grant before the payment voucher, so an unauthorized caller never reaches settlement.

Writing capabilities

A grant holds one or more capabilities. A call is allowed when any one of them matches.
The provider refuses any call (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 an approveGrant 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
The approval may also return 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
When the hook is set, the provider’s 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 with delegateGrant. Name the held grant by the id of the last grant in its chain:
caller.ts
The returned chain is the held chain with the new grant appended. Hand it to the other node, which stores it with 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.
The provider never sees the delegation happen. It checks the whole chain on each call.

Expiry and renewal

Each copy of a grant lasts lifetimeMs, 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.
Where a revocation happens matters. On the provider, it takes effect on the next call. On a delegating node, it only stops that node renewing the grant, because the provider never sees it, so the delegated grant keeps working until its current copy expires. To cut off a delegated grant at once, revoke it on the provider with its issuer’s key.
Unrestricted, free functions stay open to everyone, blocked keys included, because their callers don’t identify themselves.

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.