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:
| Status | Meaning |
|---|---|
pending | Nobody has decided yet. |
approved | The owner approved it. |
denied | The owner denied it. |
withdrawn | The app that filed it took it back before a decision. Nobody decided it. |
-
Decided once. A request leaves
pendingonce. Approving, denying or withdrawing it again returns409. -
A deadline to decide. A request can be decided until its
expires_at, or for 7 days aftercreated_atif you set noexpires_at. After that, approve and deny return409. Granite does not rewrite the request when the deadline passes: itsstatusstayspending, so compare the deadline yourself if you need to know. -
Request types.
request_typesays whether approval creates a grant:request_typeCreates a grant? Grant service_idone_time_action(default)No none storage_grantYes storage:{provider}, fromrequested_resourcecapability_grantYes spendfor aspend:resource, elsecapability_gatewaydelegation_grantYes pigeonA
storage_grantmust name its resource asstorage:{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_uidisnull, and yourx-user-idis kept asrequester_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-idrefers to them. Your later requests with thatx-user-idare bound when filed (owner_bound: truein the create response). - To set up that link before you have anything to ask, create a
connection (
POST /v1/connections) and send the person tohttps://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 atGET /v1/transparency/jwks:curl -fsS https://granitegranted.com/v1/transparency/jwksNot 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 asrequester_app_id. That grant’ssubject_kindischirp_client; - its resource (
resource, plusresourcesif the request listed more); - its scopes: the ones the owner approved;
- its expiry: the request’s
expires_at, or none; - its status:
activeorrevoked.
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
activeand not past its expiry; - the
resourceyou 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_idmust be your app, or the answer isapproved: false. - You may name yourself with
subject_app_id(or, with a machine token, your ownsubject_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_clientgrant matches only when an enforcer names, insubject_client_id, theclient_idof the person’s token it was shown. Naming nobody, or naming the client assubject_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_subon every grant-creating request. A request that needs a passkey and has norequester_chirp_subcannot 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,environmentand the decider. A request that names morerequested_resourcesthan 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 whetherdecided_bymay 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.