Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Concepts

This page describes the rules Granite applies today.

Approval requests

An approval request is one question to one person. Its status is one of:

StatusMeaning
pendingNobody has decided yet.
approvedThe owner approved it.
deniedThe owner denied it.
withdrawnThe app that filed it took it back before a decision. Nobody decided it.
  • Decided once. A request leaves pending once. Approving, denying or withdrawing it again returns 409.

  • A deadline to decide. A request can be decided until its expires_at, or for 7 days after created_at if you set no expires_at. After that, approve and deny return 409. Granite does not rewrite the request when the deadline passes: its status stays pending, so compare the deadline yourself if you need to know.

  • Request types. request_type says whether approval creates a grant:

    request_typeCreates a grant?Grant service_id
    one_time_action (default)Nonone
    storage_grantYesstorage:{provider}, from requested_resource
    capability_grantYesspend for a spend: resource, else capability_gateway
    delegation_grantYespigeon

    A storage_grant must name its resource as storage:{provider}:{resource}.

Who owns a request

Granite never takes an app’s word for who a person is. Your x-user-id names a ChirpAuth account; only that account, signed in to Granite, can own the request.

  • Name the person by the ChirpAuth subject id (sub) your app received when they signed in to it with ChirpAuth. An id ChirpAuth cannot match to an account can never be claimed.
  • A request you file with an app credential or machine token starts unbound, unless the person has already linked your x-user-id (see below): owner_uid is null, and your x-user-id is kept as requester_user_ref.
  • It becomes bound when the person opens its link (https://granitegranted.com/consent/{id}) while signed in to Granite with the account you named. Anyone else who opens it is told the request was filed for a different account, and nothing changes.
  • When a person claims one of your requests, Granite remembers that your x-user-id refers to them. Your later requests with that x-user-id are bound when filed (owner_bound: true in the create response).
  • To set up that link before you have anything to ask, create a connection (POST /v1/connections) and send the person to https://granitegranted.com/consent/connections/{id} to confirm it. Only the account you named can confirm it.

Decisions

  • Only the owner decides. Approve and deny accept only a person’s own Granite sign-in (see Authentication). App credentials and machine tokens get 403, so an app cannot approve its own request. Granite checks the credential, not who is holding it.

  • Approval can narrow, never widen. The owner may approve fewer scopes than you asked for. Approving a scope you did not request returns 400.

  • Signed record. Granite signs each decision with an Ed25519 key and links it to the same person’s previous decision (prev_hash). The public key is published at GET /v1/transparency/jwks:

    curl -fsS https://granitegranted.com/v1/transparency/jwks
    

    Not yet: these docs do not describe the signed form, so you cannot check a signature from them alone.

Grants

Approving a grant-creating request creates one grant. A grant records:

  • its subject: the app that asked (subject_app_id) and, if the request named one, the agent (subject_chirp_sub). A person can also grant to a public ChirpAuth client, such as a browser app with no credential of its own, by filing the request with "requester_kind": "chirp_client" and the client id as requester_app_id. That grant’s subject_kind is chirp_client;
  • its resource (resource, plus resources if the request listed more);
  • its scopes: the ones the owner approved;
  • its expiry: the request’s expires_at, or none;
  • its status: active or revoked.

Revoking is one-way: a revoked grant never becomes active again. The owner can revoke any of their grants; an app can revoke a grant whose subject is that app, to give up authority it holds.

Checking a grant

POST /v1/grants/verify answers “may this actor do this now?”. It returns "approved": true only if all of these hold:

  • the grant is active and not past its expiry;
  • the resource you send, if any, is one the grant covers (exact match);
  • every scope you send is one the grant holds (case is ignored);
  • the actor matches the grant’s subject.

Who the actor is:

  • Without the enforcer privilege, naming nobody makes the actor you: the grant’s subject_app_id must be your app, or the answer is approved: false.
  • You may name yourself with subject_app_id (or, with a machine token, your own subject_chirp_sub).
  • Naming a different app or agent means you are enforcing someone else’s grant. That needs the enforcer privilege, which Stackwell grants per service; without it the call returns 403.
  • A chirp_client grant matches only when an enforcer names, in subject_client_id, the client_id of the person’s token it was shown. Naming nobody, or naming the client as subject_app_id, does not match it.

You can also verify by request_id. For a request that created a grant, the answer is about that grant. For a one_time_action, it is about the request itself, and you must name your app in subject_app_id.

A grant made in one environment never verifies in another: a test grant can’t authorize a production action. Every credential Granite issues to apps works in production ("environment": "Prod").

Warden passkeys

Some approvals also need a passkey confirmation, called a Warden assertion. Granite decides which requests need one.

  • What you must send: name the agent that will use the grant in requester_chirp_sub on every grant-creating request. A request that needs a passkey and has no requester_chirp_sub cannot be approved (400).
  • How the person confirms: with a passkey they registered as a Warden key, which signs a challenge Granite issues for that request.
  • If the person deciding has no Warden key: Granite’s approval screen offers no Approve button and links to registering one; they can still deny. An approval without a valid assertion is refused (403).

Some one-time actions need one too: those the resource’s owner service lists as irreversible (today, Drive’s crypto_shred on drive:owner:<uid>).

  • What it proves: the deciding person’s Warden key signed a challenge over this request’s id, requester_app_id, requested_action, requested_resource, environment and the decider. A request that names more requested_resources than its primary cannot be approved (400).
  • What you get back: the approved request carries "warden_approval": {"decided_by": "...", "assertion": {...}}, so the service that performs the action can check the signature itself, against keys it trusts, and decide whether decided_by may authorize it.

Decision webhook

If you set callback_url, Granite POSTs the outcome there when the owner approves or denies. It is one attempt with a 5-second timeout and no retry. It is not sent for withdrawal or for an expired deadline. Treat it as a hint and read the request for the result. See API examples for the payload and signature.