Skip to main content

Crate didbot_scope

Crate didbot_scope 

Source
Expand description

The granular atproto OAuth scope grammar, parsed into a typed representation with intersection and containment operations.

plan/oauth.md calls this “the single most important thing” this epic produces, because plan/scope-policy.md has to intersect a request against a per-agent ceiling and report exactly which scope was refused — impossible to do correctly over bare strings, where repo:app.bsky.feed.* and repo:app.bsky.feed.post are unrelated text but one contains the other. Everything here is inert with respect to policy: this module parses the wire grammar, tests whether one Scope contains another, and intersects a requested ScopeSet against a ceiling ScopeSet. It does not decide what a ceiling is — that is plan/scope-policy.md, blocked on this and on policy-store, and it is deliberately not built here.

§The grammar

Space-separated scope strings, one atom each:

  • atproto — the base scope every atproto OAuth grant carries.
  • transition:generic, transition:chat.bsky, transition:email — the legacy, ungranular scopes the spec keeps for compatibility. Each is modelled as its own variant rather than desugared into the granular scopes it stands in for, because plan/scope-policy.md is explicit that transition:* is the one exception every granular rule has to reckon with by name, not the one this module should quietly launder away.
  • repo:<collection>[?action=create&action=update…] — write access to one collection. Several actions repeat the parameter, as the permission spec writes every list parameter; no action query means all three.
  • rpc:<lxm>?aud=<aud> — permission to call one XRPC method at another service: the audience is a DID and a service fragment, with its # written %23, or * for every audience.
  • blob:<mime>, or blob?accept=<mime>[&accept=<mime>…] — permission to upload a blob of one MIME type or type prefix. The two spellings are the same scope: the positional one names a single type, the accept one names as many as it repeats. How large a blob may be is not in this grammar; it is the operator’s, in policy and in the server’s blob limit.
  • identity:<attr> — control of the account’s identity: IdentityAttr names the handle, or * for the whole DID document and handle. plan/scope-policy.md never grants one.
  • account:<attr>[?action=<action>] — the account’s hosting: AccountAttr names what, and AccountAction how much, read when the atom names none. Never granted either.
  • include:<nsid>[?aud=<did>%23<service>] — a permission set, published as a lexicon. As an atom it admits only itself. Include reads it, and Include::grants turns the set’s published permissions into the repo: and rpc: atoms it grants.

Every value may be percent-encoded, and every positional value may be written as a parameter instead: repo?collection=a&collection=b, rpc?lxm=*&aud=…, identity?attr=handle, include?nsid=…. A list parameter names one scope per value, so repo?collection=a&collection=b reads as repo:a repo:b, and that is how it prints.

mime patterns may wildcard either half of type/subtype (image/* contains image/png).

§Forms the spec does not define

ScopeSet::read reads an atom written in a form the permission spec does not define as well as it can, and says how in a ScopeWarning. One such form is a partial wildcard, some.prefix.*, which this grammar reads as the prefix and every NSID beneath it at any depth: an operator’s ceiling uses it without a warning.

An atom this grammar cannot read at all — an unknown kind, or a value its kind does not define — is kept as Scope::Unknown, which admits only itself, rather than failing the whole string. A request carrying one can then be judged on the rest. didbot-serve never grants one.

Modules§

account 🔒
account: scopes, as the permission spec defines them: one attribute of the account’s hosting, and how much control over it.
action 🔒
action= values, and the set a scope carries.
error 🔒
The limits a scope string is read under, and every way reading one fails.
identity 🔒
identity: scopes, as the permission spec defines them: control of the account’s DID document and handle.
include 🔒
What an include: scope names, and what the permission set it names grants.
parse 🔒
Splitting one atom into its kind, its positional part and its parameters, as the permission spec’s “Scope String Syntax” writes them.
pattern 🔒
The NSID and MIME patterns a scope’s positional value may be.
read 🔒
Reading one atom: the scopes it names, and the warnings its form carries.
scope 🔒
One parsed scope atom: what it is, what it admits, and how it prints.
set 🔒
A canonical set of atoms, and the intersection a ceiling is applied with.
transition 🔒
The three legacy transition:* scopes and what each subsumes.
warning 🔒
How an atom written in a form the permission spec does not define is read.

Structs§

Include
An include: scope, read: the permission set it names, and the audience that set’s rpc permissions may inherit.
MimePattern
A MIME pattern: type/subtype, with either half (or both) as *.
ScopeRefused
The exact scope a ceiling refused, for insufficient_scope.
ScopeSet
A space-separated set of Scopes, as carried on a scope parameter or a token response.

Enums§

AccountAction
How much control an account: scope grants over its attribute.
AccountAttr
The part of an account’s hosting an account: scope names.
Action
One action a repo: scope’s action= query may name. account: has its own, in crate::AccountAction.
ActionSet
The set of actions a repo: scope grants.
IdentityAttr
The part of an account’s identity an identity: scope names.
IncludeError
Why an include: scope names no permission set.
NsidPattern
An NSID pattern: exact, a recursive prefix wildcard, or the bare *.
Scope
One parsed scope atom.
ScopeParseError
A failure parsing one scope atom or a whole scope string.
ScopeWarning
One way an atom’s form departs from the permission spec, and how this grammar reads it anyway. A spec form carries none.
Transition
The three legacy transition:* scopes.

Constants§

MAX_ATOM_BYTES
The longest atom Scope::parse reads: room for a longest NSID (317 bytes) with a longest DID (2048 bytes) as its aud.
MAX_GRANT_ATOMS
The most atoms ScopeSet::parse_grant reads. Four times MAX_SCOPE_ATOMS, for the same reason as MAX_GRANT_BYTES.
MAX_GRANT_BYTES
The longest grant ScopeSet::parse_grant reads: a request with each include: replaced by the atoms its permission set grants.
MAX_SCOPE_ATOMS
The most scopes a string ScopeSet::parse reads may name. An atom that lists several collections or methods names one for each. ScopeSet::new compares every pair of scopes, so this also bounds its cost.
MAX_SCOPE_BYTES
The longest scope string ScopeSet::parse reads. A real request is a few hundred bytes.